- Elixir 97%
- JavaScript 1.7%
- CSS 0.5%
- HTML 0.4%
- Dockerfile 0.2%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| assets | ||
| config | ||
| docs | ||
| lib | ||
| priv | ||
| rel/overlays/bin | ||
| scripts/screenshots | ||
| test | ||
| .dockerignore | ||
| .formatter.exs | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| cliff.toml | ||
| Dockerfile | ||
| mise.toml | ||
| mix.exs | ||
| mix.lock | ||
| README.md | ||
rM Analyzer
Mirrors a reMarkable cloud library, shows notebooks and PDFs page by page, transcribes handwriting into Markdown with Claude, and answers questions about your notes in a chat. Everything is behind sign-in with an OpenID Connect provider like Pocket ID, and each user has their own library, OCR setup, transcriptions, search and chats.
It can also make suggestions: the notebook each finished Quick sheets page
belongs in, which it then moves or copies there, and tags for notebooks and
books with few or none. Once you approve, it writes them to your reMarkable
cloud, through the same unofficial API the sync reads with; before each
write it saves the cloud's index in a snapshots directory next to the
mirrored files. Both are off until you turn them on in settings. See
docs/quick-sheets-sorting.md and docs/tag-suggestions.md.
Ask the chat to put something in a notebook (a summary of some pages, say)
and it drafts a new page of typed text, shown as the tablet will show it.
Add it, and it goes at the end of the notebook, written to your reMarkable
cloud the same way. See docs/typed-pages.md.
Libraries can be backed up, daily or on demand, to a folder on the server
or an S3 bucket, as deduplicated snapshots. See docs/backups.md.
AI agents like Claude Code can reach your notes too, over MCP at /mcp,
with an API key made in Settings → API & MCP, which also shows how to set
up common agents. A key sees only its user's library, read only unless
you let it add pages to notebooks. See docs/mcp.md.
More screenshots, all of mock data: a notebook and its transcription, a PDF with notes, a typed mirror, chat, search, tags, Quick sheets suggestions, tag suggestions, backups, a backup snapshot, OCR settings, API & MCP settings, status, and on a phone, the library and a notebook.
Development
Tool versions are pinned in mise.toml (Erlang, Elixir, Node). You also need
PostgreSQL, pdftotext (poppler) to read the printed text of PDFs and
EPUBs: dnf install poppler-utils, apt install poppler-utils or
brew install poppler, and for OCR:
rsvg-convert(librsvg) to rasterize pages:dnf install librsvg2-tools,apt install librsvg2-binorbrew install librsvg- the Claude Code CLI (
claude), for the Claude Code OCR provider
Then:
mix setup
mix phx.server
and visit localhost:4000. Run mix precommit before
committing.
The screenshots in docs/screenshots are of the app on mock data, taken by
scripts/screenshots/run.sh; run it again after changing the UI. See
scripts/screenshots/README.md.
Signing in
Everything is behind sign-in with an OpenID Connect provider, such as
Pocket ID. For development, register a client there
with the callback URL http://localhost:4000/auth/oidc/callback, then copy
config/dev.secret.exs.example to config/dev.secret.exs (git ignores it)
and fill it in. The OIDC_* environment variables below work too, and win
over that file.
Running in production
The Dockerfile builds a self-contained image, including the Claude Code CLI,
rsvg-convert and pdftotext:
docker build -t rm-analyzer .
docker run -p 4000:4000 -v rm-analyzer-data:/data \
-e DATABASE_URL=ecto://user:pass@db/rm_analyzer \
-e SECRET_KEY_BASE=... -e ENCRYPTION_KEY=... -e PHX_HOST=notes.example.com \
rm-analyzer
Migrations run on start. Mount a volume at /data: mirrored files and page
renders are stored there, and would otherwise be downloaded again after every
restart. Each user pairs their reMarkable and sets up their OCR provider in
the web UI, under Settings. The server can supply the OCR provider's token for
everyone instead, with RM_CLAUDE_CODE_OAUTH_TOKEN or RM_ANTHROPIC_API_KEY
(see below).
Sign-in needs an OpenID Connect provider, and the app won't start without
one. In Pocket ID, add an OIDC client with the callback URL
https://<PHX_HOST>/auth/oidc/callback, and restrict it to the user groups
that should get in: anyone the provider lets through can sign in, and gets
an account on first sign-in. On an instance upgraded from before users had
their own libraries, the first user to sign in gets what was already there.
Environment
| Variable | Required | |
|---|---|---|
DATABASE_URL |
yes | ecto://USER:PASS@HOST/DATABASE |
DATABASE_SSL |
true for TLS to the database, verifying its certificate against the system CAs (same as ?ssl=true on the URL). |
|
DATABASE_SSL_CACERTFILE |
Path to a CA certificate, for TLS to a database whose certificate comes from a private CA. The hostname in DATABASE_URL must match the certificate. |
|
SECRET_KEY_BASE |
yes | Signs cookies. Generate with mix phx.gen.secret. |
ENCRYPTION_KEY |
yes | Encrypts the reMarkable and OCR tokens stored in the database. Generate with openssl rand -base64 32. Changing it makes the stored tokens unreadable, so you'd have to pair and set up OCR again. |
PHX_HOST |
yes | The hostname the app is reached at, over HTTPS. LiveView refuses connections from other hosts, and the OIDC callback URL and the MCP address shown to agents are built from it. |
OIDC_ISSUER |
yes | The OpenID Connect provider's URL, like https://id.example.com. |
OIDC_CLIENT_ID |
yes | The client's ID at the provider. |
OIDC_CLIENT_SECRET |
The client's secret. Leave unset for a public client; PKCE is always used. | |
OIDC_PROVIDER_NAME |
Shown on the sign-in button, like Pocket ID. |
|
TZ |
The time zone times are shown in, as an IANA name like America/New_York. Defaults to the machine's zone, which is UTC in the image. |
|
PORT |
HTTP port, default 4000. The app speaks plain HTTP; put TLS in a reverse proxy. |
|
BACKUP_DIR |
A directory users can back their libraries up to, each in a folder of their own; without it they can only back up to S3. Mount it from another disk than /data. |
|
BACKUP_S3_ALLOWED_NETWORKS |
S3 endpoints on private or reserved networks (loopback, the LAN, link-local like cloud metadata) are refused, so users can't reach internal services through backups. List networks to allow anyway, as CIDRs separated by commas, like 192.168.1.0/24,10.0.0.5 for MinIO on your LAN. |
|
REMARKABLE_BLOB_DIR |
Where mirrored files go, default /data/remarkable/blobs in the image. Renders go in a sibling renders directory, and cloud snapshots taken before sorting writes in snapshots (the newest 50). |
|
RUN_MIGRATIONS |
Set to false to skip migrations on start and run bin/migrate yourself. |
|
POOL_SIZE |
Database connections, default 10. |
|
RM_CLAUDE_CODE_OAUTH_TOKEN |
A token from claude setup-token, used for every user's Claude Code provider instead of one saved in Settings (where the field is then locked). It applies to all users, so share it only on an instance that's just yours. |
|
RM_ANTHROPIC_API_KEY |
An Anthropic API key, used the same way for every user's Claude API provider; all their usage is billed to it. | |
OCR_CONCURRENCY |
Pages transcribed at once, default 4. More finishes a backlog sooner and uses a Claude subscription's allowance up faster; with Claude Code, each is a claude process on this machine. |
|
SORTING_CONCURRENCY |
Quick sheets pages classified at once, default 2. |
|
TAGGING_CONCURRENCY |
Items looked at for tag suggestions at once, default 2. |
|
ECTO_IPV6 |
true to connect to the database over IPv6. |
|
DNS_CLUSTER_QUERY |
For clustering several nodes via DNS. |
Releasing
Commit messages follow conventional commits
(feat:, fix:, ...), which git-cliff turns into
CHANGELOG.md (configured in cliff.toml). To release:
mise run release -n # preview the next version and its notes
mise run release # bump mix.exs, update the changelog, commit, tag and push
mise run release minor # or force major/minor/patch, or an exact 1.2.3
The version is picked from the commits since the last tag: feat bumps the
minor version, fix the patch, and breaking changes the major. The tag is
signed and carries the release notes; pushing it builds the images.
CI and images
Forgejo Actions: .forgejo/workflows/ci.yml runs the tests against Postgres
on every push to main and on pull requests. Pushing a v* tag runs
release.yml, which builds amd64 and arm64 images and publishes them to
git.knope.dev/jfro/rm-analyzer as latest, X.Y.Z, X.Y and X.
Build arguments
CLAUDE_CODE_VERSION picks the Claude Code CLI (latest by default, or
stable, or an exact version like 2.1.283). ELIXIR_VERSION,
OTP_VERSION, DEBIAN_VERSION and NODE_VERSION should track mise.toml.
