A self-hosted, review-only HTTP bridge between an Anki collection and a Light
Phone client. It embeds the official Anki pylib (anki==25.9.5) and exposes a
small API for fetching cards and submitting ratings.
License: AGPL-3.0-or-later (embedding pylib requires it).
Safety model: all collection writes go through Anki's own scheduler and sync code — the bridge never mutates the collection or revlog itself. The phone only fetches due cards and submits ratings; those ratings are applied by Anki's scheduler and propagated to your sync server unchanged.
Settings come from the environment (Settings.from_env()):
| Env var | Required | Purpose |
|---|---|---|
BRIDGE_COLLECTION |
yes | Path to the .anki2 collection file. |
BRIDGE_TOKEN |
yes | Bearer token clients must present. |
BRIDGE_DATA_DIR |
no | Bridge-local state (answer-dedup db); NOT the collection dir. Default /data/bridge. |
SYNC_ENDPOINT |
no | Sync server URL (e.g. http://sync-server:8080/). |
SYNC_USERNAME |
no | Sync account username. |
SYNC_PASSWORD |
no | Sync account password. |
If the three SYNC_* vars are unset (or partially set), sync is disabled and
studying still works entirely against the local collection — study/start and
study/finish simply report synced: false, "sync not configured". Sync failures
are also non-fatal: a failed sync never aborts a study session.
A brand-new bridge collection cannot normal-sync against a sync server until one
full sync has established a shared baseline. Until you resolve this, the bridge
detects a full-sync-required state and /v1/study/start returns 503
needs_attention.
Resolve it once, with the anki-bridge-init CLI, picking the direction:
anki-bridge-init download— the server already holds your collection (the usual case: you have been using Anki elsewhere). The server wins.anki-bridge-init upload— a fresh/empty sync server, and the bridge holds the collection to seed. The bridge wins.
Both directions are destructive to one side (they discard the other side's collection). The CLI prints exactly which side it will discard and waits for you to press Enter to confirm (Ctrl-C, EOF, or piped-closed stdin aborts cleanly).
needs_attention means the bridge and the sync server have diverged and a full
sync is required to reconcile them. The bridge never auto-resolves this:
auto-picking a direction could silently discard review history on one side. It
sets the flag, blocks /v1/study/start with 503, and waits for the operator to
run anki-bridge-init download or upload. Once the full sync completes, normal
syncing resumes.
Uvicorn factory (see the Dockerfile CMD):
uvicorn anki_bridge.app:app_from_env --factory --host 0.0.0.0 --port 8000
Or via docker compose:
BRIDGE_TOKEN=<your-token> docker compose up -d
Note: the docker-compose.yml pins a ghcr.io/ankitects/anki-sync-server image
tag that could not be validated in the build environment — validate it on first
deploy. If the tag is missing, list available tags (see the comment in
docker-compose.yml) and pin the nearest same-minor version. Fallback if no
official image works: run the server straight from pylib with
python -m anki.syncserver, setting SYNC_USER1 / SYNC_BASE / SYNC_HOST /
SYNC_PORT. That is the exact path the round-trip integration test validates.
- Answers are idempotent by client UUID. Re-submitting the same UUID returns
duplicateand applies nothing a second time — safe to retry. - Batch truncation on unexpected error. If applying an item raises an
unexpected error, the batch response is truncated: that item gets
errorand every item after it gets no result entry (no ack). Clients retry un-acked items by UUID. staleanswers must be dropped, not retried. Astaleresult means the card was already reviewed elsewhere (the states the phone echoed were superseded). Re-submitting will only returnstaleagain — drop it.
pytest # unit suite (no sync server needed)
pytest -m sync # round-trip tests; needs a running sync server (port 18080)
For -m sync, run a sync server first: docker compose up -d sync-server, or the
validated fallback python -m anki.syncserver with SYNC_USER1=test:test123 on
port 18080.
tests/fixtures/local/*.apkg is a gitignored slot for dropping in a personal-deck
corpus to run sweeps against real cards.