Remarkable cloud sync & document viewer & analyzer
  • Elixir 97%
  • JavaScript 1.7%
  • CSS 0.5%
  • HTML 0.4%
  • Dockerfile 0.2%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-18 13:37:13 +00:00
.forgejo/workflows chore: Erlang 29.1.2, Node 24.22.0, Debian trixie-20261005 base images 2026-10-17 14:22:53 -04:00
assets chore(deps): daisyUI 5.7.50 2026-10-17 14:26:25 -04:00
config feat: a Status page and header alert for what's failing: syncs, backups, the handwriting provider, and background jobs, with retry and dismiss (#40) 2026-10-17 14:03:12 -04:00
docs docs: a script that takes the README and docs screenshots on mock data 2026-10-17 16:58:08 -04:00
lib fix: the sync status is in the header of every page, not just the library's 2026-10-17 15:46:40 -04:00
priv feat: typed mirrors of handwritten pages, right after them, kept in step with their transcription (#25) 2026-10-17 15:37:58 -04:00
rel/overlays/bin Add production Docker image and Forgejo CI 2026-09-28 10:06:46 -04:00
scripts/screenshots docs: a script that takes the README and docs screenshots on mock data 2026-10-17 16:58:08 -04:00
test fix: the sync status is in the header of every page, not just the library's 2026-10-17 15:46:40 -04:00
.dockerignore Add production Docker image and Forgejo CI 2026-09-28 10:06:46 -04:00
.formatter.exs docs: a script that takes the README and docs screenshots on mock data 2026-10-17 16:58:08 -04:00
.gitignore docs: a script that takes the README and docs screenshots on mock data 2026-10-17 16:58:08 -04:00
AGENTS.md feat(auth): sign in with OpenID Connect 2026-09-28 11:51:04 -04:00
CHANGELOG.md chore(release): v1.2.0 2026-10-17 15:58:51 -04:00
cliff.toml chore: leave merge commits out of the changelog 2026-10-15 15:30:24 -04:00
Dockerfile chore: Erlang 29.1.2, Node 24.22.0, Debian trixie-20261005 base images 2026-10-17 14:22:53 -04:00
mise.toml chore: Erlang 29.1.2, Node 24.22.0, Debian trixie-20261005 base images 2026-10-17 14:22:53 -04:00
mix.exs chore(release): v1.2.0 2026-10-17 15:58:51 -04:00
mix.lock chore(deps): oban 2.24.2, req 0.7.5, finch 0.24, swoosh 1.29, phoenix_pubsub 2.4.1, mdex 0.14.2, rustler_precompiled 0.10, dns_cluster 0.3.1 2026-10-17 14:16:59 -04:00
README.md docs: the library's screenshot at the top of the README, and links to the rest 2026-10-17 22:22:53 -04:00

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.

Your library: folders, and notebooks shown as their first pages

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-bin or brew 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.