Anki client for Kindle built on top of the official AnkiWeb sync protocol
356
AnkiWeb client for Amazon Kindle and other e-ink browsers. AnkiPaper lets you sign in to AnkiWeb, browse decks, and review cards in a server-rendered interface designed for slow monochrome devices.
It uses Anki's native collection format, the official AnkiWeb sync protocol, and the same Rust scheduler backend used by Anki Desktop. Card editing, analytics, and heavy client-side UI are intentionally out of scope.
New / Learn / Review counts from the FSRS scheduler.Again / Hard / Good / Easy ratings and interval previews..jpg, .jpeg, .png, .webp, .otf, .ttf, .woff, and .woff2 files are synchronized. Audio, video, JavaScript, and other media are intentionally skipped.TZ=Europe/Moscow (any IANA name) to .env and restart to shift the boundary to local time.AnkiPaper is an unofficial third-party client. It is not affiliated with Ankitects Pty Ltd and is not on the list of approved clients in the AnkiWeb Terms of Service.
The AnkiWeb ToS restricts direct access to the sync service to approved clients (Anki Desktop, AnkiMobile, AnkiDroid, AnkiUniversal). AnkiPaper uses that same sync protocol to talk to AnkiWeb directly, which is outside the approved-clients list. Ankitects have historically tolerated small personal-use clients that only sync their own data and do not add noticeable load, but they may change their policy or suspend accounts at their sole discretion.
Practical implications:
The application image serves HTTP on port 8000. TLS is expected to be terminated by an external reverse proxy.
Clone repository. Then from the repository root:
cp deploy/.env.example .env && cp deploy/docker-compose.yml docker-compose.yml
docker compose up --build
The Compose stack starts:
ankipaper — FastAPI application on 127.0.0.1:8000;redis — Redis rate-limit backend.The application is available at http://localhost:8000. Persistent data is stored in ./.data/; Redis persistence is stored in ./.redis/.
Settings are read from environment variables or .env using the ANKIPAPER_ prefix. deploy/.env.example contains an example.
| Variable | Default | Purpose |
|---|---|---|
ANKIPAPER_COOKIE_MAX_AGE_DAYS | 30 | Signed session lifetime in days. |
ANKIPAPER_TRUSTED_UPSTREAM_HTTPS_PROXY | false | Set to true behind a TLS-terminating reverse proxy (nginx, cloudflared, etc.). Enables Secure cookies, X-Forwarded-For IP trust, and proxy-aware canonical URLs. |
ANKIPAPER_COOKIE_INSECURE_HOSTS | "" | Comma-separated list of Host header values (exact match or *.suffix wildcard) for which the session cookie is set without Secure. Use this when the same process serves both Cloudflare-fronted HTTPS and a direct LAN access over plain HTTP. |
ANKIPAPER_SHOW_PRIVACY_POLICY | false | Adds a link to /static/privacy_policy.html in login and deck-list footers. |
ANKIPAPER_CONTACTS_EMAIL | "" | Email address shown as a "Contact Us" link in the page footer. Empty hides the link. |
ANKIPAPER_REDIS_URL | redis://localhost:6379/0 | Redis URL for login and sync rate limiting. In Docker Compose use redis://redis:6379/0. |
ANKIPAPER_LOGIN_IP_MAX_ATTEMPTS | 5 | Maximum login attempts per IP in the IP window. |
ANKIPAPER_LOGIN_IP_WINDOW_SECONDS | 60 | Login rate-limit window per IP. |
ANKIPAPER_LOGIN_USER_MAX_ATTEMPTS | 10 | Maximum login attempts per username in the username window. |
ANKIPAPER_LOGIN_USER_WINDOW_SECONDS | 3600 | Login rate-limit window per username. |
ANKIPAPER_DATA_MAX_BYTES | 0 | Maximum total /data size for creating new accounts. 0 disables the limit; existing accounts remain accessible. |
ANKIPAPER_MEDIA_MAX_FILE_BYTES | 1048576 (1 MiB) | Maximum size of one media file. Larger files are skipped during media sync. |
ANKIPAPER_METRICS_USERNAME | "" | HTTP Basic username required by the Prometheus scraper. Empty disables /metrics with HTTP 503. |
ANKIPAPER_METRICS_PASSWORD | "" | HTTP Basic password required by the Prometheus scraper. Empty disables /metrics with HTTP 503. |
ANKIPAPER_PUBLIC_URL | "" | Public origin (for example, https://ankipaper.study) used in canonical, Open Graph, and sitemap URLs. When empty, the origin is derived from the incoming request (requires ANKIPAPER_TRUSTED_UPSTREAM_HTTPS_PROXY=true). |
Redis is required for protected login and sync operations. If Redis is unavailable, rate limiting fails closed: login returns an error and sync is blocked rather than running without protection.
The Compose service binds port 8000 to loopback only. Put nginx, Cloudflare Tunnel, or another TLS reverse proxy in front of it for external access.
For a reverse-proxy deployment:
ANKIPAPER_TRUSTED_UPSTREAM_HTTPS_PROXY=true.X-Forwarded-For header carries the real client IP. The application also understands Cloudflare's CF-Connecting-IP header when proxy mode is enabled.If the same instance is also reachable over plain HTTP from the LAN, list the LAN hostnames in ANKIPAPER_COOKIE_INSECURE_HOSTS so the session cookie is set without the Secure flag for those hosts:
ANKIPAPER_TRUSTED_UPSTREAM_HTTPS_PROXY=true
ANKIPAPER_COOKIE_INSECURE_HOSTS=ankipaper.lan,192.168.1.10
Set both ANKIPAPER_METRICS_USERNAME and ANKIPAPER_METRICS_PASSWORD to expose GET /metrics for Prometheus scrapers. With either credential left empty, the endpoint returns 503 instead of exposing data unauthenticated.
Content type
Image
Digest
sha256:e4c0411b9…
Size
93 MB
Last updated
1 day ago
docker pull oixa/ankipaper