{"id":50965762,"url":"https://github.com/aitsc/ankiweb","last_synced_at":"2026-06-18T20:02:23.889Z","repository":{"id":362345568,"uuid":"1257784764","full_name":"aitsc/ankiweb","owner":"aitsc","description":"Self-hosted web port of Anki: study, browse, edit \u0026 manage flashcards from any browser, plus the full AnkiConnect HTTP API. Built on the official anki Python lib + FastAPI, reusing Anki's real frontend (reviewer/editor/graphs). Single-user, local-first, LAN-ready. Spaced repetition (SRS) in the browser — no sync, no add-ons.","archived":false,"fork":false,"pushed_at":"2026-06-03T20:13:16.000Z","size":1120,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-03T21:07:33.972Z","etag":null,"topics":["anki","anki-connect","ankiconnect","fastapi","flashcards","learning","python","self-hosted","spaced-repetition","srs","study-tool","web-app"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aitsc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T02:20:44.000Z","updated_at":"2026-06-03T20:13:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aitsc/ankiweb","commit_stats":null,"previous_names":["aitsc/ankiweb"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/aitsc/ankiweb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aitsc%2Fankiweb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aitsc%2Fankiweb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aitsc%2Fankiweb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aitsc%2Fankiweb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aitsc","download_url":"https://codeload.github.com/aitsc/ankiweb/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aitsc%2Fankiweb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34505423,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anki","anki-connect","ankiconnect","fastapi","flashcards","learning","python","self-hosted","spaced-repetition","srs","study-tool","web-app"],"created_at":"2026-06-18T20:02:23.018Z","updated_at":"2026-06-18T20:02:23.882Z","avatar_url":"https://github.com/aitsc.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ankiweb\n\n\u003e ⚠️ **Unofficial, personal, single-user project — not affiliated with Anki/Ankitects.**\n\u003e This is an independent, community browser port of [Anki](https://apps.ankiweb.net),\n\u003e intended to be run by **one user on their own machine**. It is **NOT** affiliated with,\n\u003e endorsed by, or connected to Ankitects Pty Ltd, and it is **NOT** the official **AnkiWeb**\n\u003e sync service at apps.ankiweb.net. \"Anki\" and \"AnkiWeb\" are names of that upstream\n\u003e project/service. This is a hobby/personal implementation provided as-is, with no warranty,\n\u003e under AGPL-3.0-or-later (see [LICENSE](LICENSE) and [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)).\n\nA **browser port of Anki desktop + AnkiConnect**, built on the official `anki` Python\npackage (pylib) + FastAPI. It serves Anki's real study UI in a browser and re-implements\nthe full AnkiConnect HTTP API — for a single user, on your own machine.\n\n**It is a faithful translation, not a rewrite.** Where Anki ships a compiled frontend\n(the SvelteKit pages for graphs / deck options / change-notetype / imports / image\nocclusion, and the `reviewer.js` / `editor.js` bundles), ankiweb **reuses the vendored\nbuild** and bridges it to the `anki` backend; the Qt-only dialogs (overview, custom study,\nfiltered-deck, export) are rebuilt as small server-rendered pages.\n\n**Scope:** everything in the desktop study/edit/manage flow + the AnkiConnect API.\n**Out of scope (by design):** sync (AnkiWeb) and add-ons/plugins.\n\n---\n\n## Screenshots\n\n**Deck browser** — your full deck tree (including nested decks), counts, and the always-present\ntop toolbar. Anki's home screen, in a browser tab.\n\n![Deck browser](docs/images/home.png)\n\n**Studying** — the real Anki reviewer (`reviewer.js` + MathJax) drives the card; ankiweb adds a\ncard-action bar (mark · bury · suspend · set due · reset · delete · undo · flags) and keyboard\nshortcuts.\n\n![Reviewer](docs/images/reviewer.png)\n\n**Browse \u0026 edit** — search, a deck/tag sidebar, the results table, and Anki's real `editor.js`\nembedded live in the detail pane (with Fields… / Cards… / Preview).\n\n![Browser](docs/images/browser.png)\n\n**Reused, not rewritten** — where Anki ships a compiled SvelteKit page, ankiweb serves the\nvendored build and wires it to the backend. Here's the full Deck Options screen, unchanged:\n\n![Deck options](docs/images/deck_options.png)\n\n**Manage Note Types** — list / add / rename / delete note types, with links into the field and\ncard-template editors (one of the Tools-menu screens ankiweb rebuilds for the web):\n\n![Manage note types](docs/images/notetypes.png)\n\n---\n\n## Requirements\n\n- Python **3.12**\n- `anki==25.9.4` (pinned — the vendored frontend must match this version; the exact upstream\n  Anki/AnkiConnect commits this port was built against are recorded in [UPSTREAM.md](UPSTREAM.md))\n- Node.js (only to build the ~2 KB shell bundle)\n- A conda env is recommended: installing `anki` pulls a newer `protobuf` that can clash\n  with other global packages, so keep it isolated.\n\n## Setup\n\n```bash\nconda create -n ankiweb python=3.12 -y\nconda run -n ankiweb pip install -e \".[dev]\"\n\n# 1. Vendor Anki's compiled frontend (downloads the aqt 25.9.4 wheel, extracts\n#    _aqt/data/web/ into ankiweb/web_assets/ — gitignored). Required.\nconda run -n ankiweb python tools/fetch_web_assets.py\n\n# 2. Build the shell bridge bundle (shell_src/bootstrap.ts -\u003e ankiweb/shell/static/bootstrap.js)\nnpm install \u0026\u0026 npm run build\n\n# 3. (optional) for the Playwright integration tests\nconda run -n ankiweb python -m playwright install chromium\n```\n\n## Run\n\n```bash\nconda run -n ankiweb python -m ankiweb\n```\n\nThis starts **two servers in one process**:\n\n| Port | Serves | Default | Configure with |\n|------|--------|---------|----------------|\n| **Web UI + WebSocket** | the browser study/edit UI and the `/ws` bridge (WS shares this port) | `127.0.0.1:8000` | `ANKIWEB_HOST` / `ANKIWEB_PORT` |\n| **AnkiConnect HTTP API** | `POST /` JSON API for AnkiConnect clients (+ Swagger docs at [`/docs`](#api-docs-swagger)) | `127.0.0.1:8765` | `ANKIWEB_AC_HOST` / `ANKIWEB_AC_PORT` (or `ankiconnect.json`) |\n\nOpen \u003chttp://127.0.0.1:8000\u003e in a browser. The AnkiConnect port defaults to **8765** on\npurpose — existing AnkiConnect clients/scripts work unchanged.\n\n## Configuration\n\nAll settings have safe localhost defaults; override via environment variables:\n\n| Variable | Default | Meaning |\n|----------|---------|---------|\n| `ANKIWEB_COLLECTION` | `~/.local/share/ankiweb/collection.anki2` | Path to the `.anki2` collection. The parent directory is created automatically; a fresh collection is created if the file doesn't exist. |\n| `ANKIWEB_HOST` | `127.0.0.1` | Web UI bind address (`0.0.0.0` to listen on all interfaces). |\n| `ANKIWEB_PORT` | `8000` | Web UI port. |\n| `ANKIWEB_ALLOWED_HOSTS` | *(empty)* | Comma-separated extra `Host` header values accepted past the DNS-rebinding guard (see **LAN access**). `*` disables the check. |\n| `ANKIWEB_AC_HOST` | `127.0.0.1` | AnkiConnect bind address (overrides `ankiconnect.json`). |\n| `ANKIWEB_AC_PORT` | `8765` | AnkiConnect port (overrides `ankiconnect.json`). |\n| `ANKIWEB_AC_KEY` | *(none)* | AnkiConnect `apiKey` (overrides `ankiconnect.json`). |\n| `ANKIWEB_IMPORT_TMP_DIR` | `\u003ccollection dir\u003e/import-tmp` | Where uploaded import/image files are staged before the backend reads them. |\n| `ANKIWEB_LANG` | *(empty → English)* | UI language, an Anki locale code (e.g. `zh-CN`, `ja`, `de`, `fr`). Chosen at startup — there is no in-app switcher; changing it means changing this var and restarting. See **Language** below. |\n| `ANKIWEB_PASSWORD` | *(empty → no password)* | If set, the web UI requires this password (a `/login` page sets a session cookie). Empty = open, the default. The AnkiConnect API keeps its own `ANKIWEB_AC_KEY`. |\n| `ANKIWEB_SOURCE_URL` | *(empty)* | AGPL §13 Corresponding-Source location for this deployment, shown on the `/about` page (only relevant if you run it as a public network service). |\n\n**`ankiconnect.json`** (optional) lives next to the collection file and uses AnkiConnect's\nown keys; environment variables override it:\n\n```json\n{ \"webBindAddress\": \"127.0.0.1\", \"webBindPort\": 8765, \"apiKey\": null,\n  \"webCorsOriginList\": [\"http://localhost\"], \"ignoreOriginList\": [] }\n```\n\n### LAN access\n\nTo reach the UI from another device, bind to all interfaces **and** allow your host\n(the Web UI has a DNS-rebinding guard that only permits localhost by default):\n\n```bash\nANKIWEB_HOST=0.0.0.0 ANKIWEB_ALLOWED_HOSTS=192.168.1.50:8000 \\\n  conda run -n ankiweb python -m ankiweb\n```\n\n`ANKIWEB_ALLOWED_HOSTS` accepts the value with or without a port (`192.168.1.50` matches\nany port), multiple comma-separated hosts, or `*` to turn the check off (only on a trusted\nnetwork). It covers both HTTP and the WebSocket bridge.\n\n### Language\n\nSet `ANKIWEB_LANG` to any Anki locale code to run the whole UI in that language — both the\nreused Anki frontend (graphs / deck options / reviewer / editor …) and ankiweb's own\nhand-written screens (deck browser, browser, Add, Preferences, etc.):\n\n```bash\nANKIWEB_LANG=zh-CN conda run -n ankiweb python -m ankiweb\n```\n\nThe language is fixed at startup (it's applied before the collection is opened); there is\nno in-app language switcher, so to change it you set `ANKIWEB_LANG` and restart. Empty or an\nunknown code falls back to English. Accepts both `zh-CN` and `zh_CN` forms.\n\n### Password\n\nBy default the web UI is open (no login) — it's a single-user, local-first app. To require a\npassword, set `ANKIWEB_PASSWORD`:\n\n```bash\nANKIWEB_PASSWORD=mysecret conda run -n ankiweb python -m ankiweb\n```\n\nVisitors then get a `/login` page; the correct password sets an httponly session cookie and\nunlocks the UI (and the `/ws` bridge). `/logout` clears it. This gates the **web app only**;\nthe AnkiConnect HTTP API (port 8765) is controlled separately by `ANKIWEB_AC_KEY`. It's a\nlight gate for LAN use, not a hardened auth system — serve over HTTPS if it matters.\n\n### API docs (Swagger)\n\nThe AnkiConnect server publishes interactive OpenAPI docs at\n\u003chttp://127.0.0.1:8765/docs\u003e (schema at `/openapi.json`). Every action has a standard\nPydantic request schema and a documented `POST /actions/\u003cname\u003e` route you can call straight\nfrom the page (\"Try it out\"), e.g. `POST /actions/findCards` with body\n`{\"query\": \"deck:French is:due\"}`.\n\nThese typed routes are an **additive convenience layer** — the canonical AnkiConnect contract\nis unchanged: real clients still `POST /` with `{\"action\", \"version\", \"params\"}`, and the\n`/actions/*` routes call the exact same dispatcher, so behavior never diverges. When\n`ANKIWEB_AC_KEY` is set, send it as the `X-API-Key` header (use the **Authorize** button in\nSwagger); the canonical `POST /` keeps reading the key from the request body as upstream does.\n\n**Extra actions (ankiweb-original).** A few actions that are *not* part of AnkiConnect live\nunder a separate `/extra_actions/\u003cname\u003e` namespace — documented in `/docs` (tagged\n`extra_actions`) and callable there, but deliberately **unknown to the canonical `POST /`**\ndispatcher (so the root surface stays byte-identical to upstream). Currently:\n\n- `POST /extra_actions/deleteModel` `{ \"modelName\": \"MyType\" }` (or `{ \"modelId\": 1234 }`) —\n  delete an entire note type (the reverse of `createModel`). Returns `true`; **errors** if any\n  note still uses it, if the type isn't found, or if it's the only remaining note type. The\n  same `X-API-Key` gate applies.\n- `POST /extra_actions/extendCardLimits` `{ \"deck\": \"MyDeck\", \"new\": 10, \"review\": -5 }` —\n  temporarily add to (or subtract from) today's new/review card limits for a deck (the API form\n  of Custom Study's \"Increase today's … card limit\"; negative reduces, deltas accumulate).\n  Identify the deck by `deck` name or `deckId`. Returns the deck's resulting counts.\n- `POST /extra_actions/getNotifyConfig` `{}` — read the [Deck push notifications](#deck-push-notifications-extras)\n  config + live status.\n- `POST /extra_actions/setNotifyConfig` `{ \"enabled\": true, \"url\": \"https://…\", \"poll_sec\": 30 }`\n  — modify that config from outside (instead of the web form). Send only the fields you want to\n  change (`enabled`/`url`/`token`/`poll_sec`/`retry_sec`/`scope`, plus optional `resync: true`);\n  omitted fields keep their value. Takes effect live; returns the resulting config + status.\n\n### Deck push notifications (Extras)\n\nAn **ankiweb-original** feature (not part of the Anki/AnkiConnect port): ankiweb can POST to an\nendpoint of yours whenever a deck's study counts change — i.e. its `new_count`, `learn_count`,\nor `review_count` moves (including bucket shifts that keep the total). Useful for a study\nbot/agent that should react without polling.\n\nConfigure it live at **Extras ▾ → Push notifications** (`/notify`) — no env vars, no restart.\nSettings persist to a `notify.json` sidecar next to the collection. Fields:\n\n| field | meaning |\n|---|---|\n| Enabled | master on/off |\n| POST URL | where to send notifications |\n| Token | sent as `Authorization: Bearer \u003ctoken\u003e` (omitted if empty) |\n| Poll interval (sec) | how often the deck state is refreshed (one `deck_due_tree()` call — scales to thousands of decks) |\n| Retry interval (sec) | how often a failed POST is resent |\n| Scope | which decks to watch in a nested tree: **Leaf only** (default — last-level decks with no subdecks) or **All levels** (every deck; a parent's counts then include its subdecks) |\n\nThe notifier is active only when *enabled* and a URL and both intervals (\u003e 0) are set. Decks are\nidentified by **full name** (`A::B::C`); counts are the same as `getDeckStats` (respect daily\nlimits). A deck notifies whenever its `(new_count, learn_count, review_count)` tuple changes —\nany of the three, even if the total stays the same (the payload's `learnable` field is simply\n`total \u003e 0`, for convenience). With *All levels*, a parent's counts are the subdeck rollup;\n*Leaf only* reports just the bottom-level decks. On start, on enable, on a URL/scope change, or\nwhen you click *Save \u0026 re-push all*, every deck with nonzero counts (in scope) is pushed once so\nyour receiver syncs; always-empty decks stay silent.\n\n**Request** ankiweb sends:\n\n```\nPOST \u003curl\u003e\nAuthorization: Bearer \u003ctoken\u003e          # omitted when token is empty\nContent-Type: application/json\n\n{ \"source\": \"ankiweb\",\n  \"ts\": 1780500000,                     # epoch seconds\n  \"changes\": [\n    { \"deck\": \"英语词汇::单词\", \"deckId\": 1780005159378, \"learnable\": true,\n      \"new_count\": 12, \"learn_count\": 3, \"review_count\": 40 } ] }\n```\n\n**Success** = HTTP `200` **and** a JSON body with `ok` exactly `true`. Anything else (non-200,\nmissing/false `ok`, non-JSON, timeout, connection error) is treated as a failure and retried\nevery *retry interval* until it succeeds. While a notification is unacknowledged, further deck\nchanges are coalesced — the resend always carries the **latest** counts, and a deck that\nreverts to its last acknowledged counts sends nothing. Deleted/renamed decks drop silently.\n\n### Night mode\n\nToggle with the 🌙 button in the top toolbar (persisted in `localStorage`); it themes the\nserver-rendered pages and threads `#night` into links to the SvelteKit pages so those\nrender dark too.\n\n### Navigation\n\nEvery server-rendered screen has an always-present top toolbar — **Decks · Add · Browse ·\nStats** (Anki's main-window toolbar, minus Sync) plus the night-mode toggle. The SvelteKit\npages (graphs, deck options, change-notetype, imports, image occlusion) are task pages\nopened from there; use the browser's back button to return.\n\n## Architecture\n\n- **`anki` pylib** owns the collection, scheduler (v3), and the Rust backend (protobuf).\n- **`ankiweb/collection_service.py`** — a single-worker, serialized wrapper around the one\n  `Collection` (pylib objects aren't thread-safe). An auxiliary pool runs the thread-safe\n  Rust calls that must be concurrent (FSRS compute/simulate + `latest_progress` polling) so\n  deck-options shows live optimize progress.\n- **`ankiweb/assets.py`** — serves the vendored Anki frontend (`/_anki/...`, `/_app/...`)\n  and routes the reused SvelteKit SPA pages.\n- **`ankiweb/anki_rpc/`** — `POST /_anki/{method}`: passthrough / custom / concurrent\n  dispatch to `col._backend.\u003cmethod\u003e_raw` (protobuf in, protobuf out).\n- **`ankiweb/bridge/`** — the WebSocket `/ws` `pycmd` bridge that the screens use to talk to\n  the server (the desktop `pycmd`/`bridgeCommand` shim, in `shell_src/bootstrap.ts`).\n- **`ankiweb/screens/`** — server-rendered pages (deck browser, overview, reviewer, browser,\n  editor, add, custom study, filtered deck, export) that mount Anki's real `reviewer.js` /\n  `editor.js` where applicable.\n- **`ankiweb/ankiconnect/`** — the AnkiConnect HTTP API (≈120 actions, minus sync).\n- **`ankiweb/__main__.py`** — runs the Web app and the AnkiConnect app as two uvicorn\n  servers on a shared collection + bridge hub.\n\n## Test\n\n```bash\nconda run -n ankiweb python -m pytest            # full suite (Playwright tests skip if chromium absent)\n```\n\nIntegration tests use Playwright + real Chromium against a live uvicorn server; install the\nbrowser once with `python -m playwright install chromium`.\n\n## Project layout\n\n```\nankiweb/            the application package (collection_service, assets, anki_rpc, bridge, screens, ankiconnect)\nshell_src/          the TS pycmd-bridge shell (compiled to ankiweb/shell/static/bootstrap.js)\ntools/              fetch_web_assets.py (vendor the frontend), build_shell.mjs (build the shell)\ntests/              pytest suite (backend + bridge + Playwright integration)\ndocs/superpowers/   design specs + implementation plans\n```\n\n## License\n\nankiweb is licensed under the **GNU Affero General Public License, version 3 or later\n(AGPL-3.0-or-later)** — see [LICENSE](LICENSE).\n\nIt is a **derivative/combined work**: it links the **Anki** Python library (`anki`,\nAGPL-3.0-or-later) at runtime, bundles and serves Anki's compiled frontend, and\nre-implements the **AnkiConnect** HTTP API (Copyright 2016–2021 Alex Yatskov,\nGPL-3.0-or-later) by closely following its source. Per GPLv3 §13 / AGPLv3 §13 these combine,\nand the project as a whole is distributed under AGPL-3.0-or-later. Upstream copyrights and\nthe permissive sub-licenses of vendored components (MathJax/Apache-2.0, jQuery/MIT,\nprotobuf.js/BSD-3, etc.) are credited in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md);\nthe GPL-3.0 text covering the AnkiConnect-derived code is in\n[LICENSES/GPL-3.0-or-later.txt](LICENSES/GPL-3.0-or-later.txt).\n\n### Source code (AGPL §13)\n\nBecause ankiweb is a network service, every user interacting with it over a network is\nentitled to its Corresponding Source. The running app exposes a **Source** link (the top\ntoolbar → `/about`). Set **`ANKIWEB_SOURCE_URL`** to where your deployed source lives so that\nlink points at the exact running version; the pinned Anki/aqt 25.9.4 source is at\n\u003chttps://github.com/ankitects/anki\u003e and AnkiConnect at \u003chttps://github.com/FooSoft/anki-connect\u003e.\n\nCopyright (C) 2026 tsc. Anki © Ankitects Pty Ltd and contributors. AnkiConnect © 2016–2021\nAlex Yatskov.\n\n\u003e **Naming note:** \"ankiweb\" collides with Anki's own **AnkiWeb** sync service and trademark.\n\u003e The AGPL covers the code but grants no trademark rights; consider renaming before any public\n\u003e release to avoid implying endorsement.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faitsc%2Fankiweb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faitsc%2Fankiweb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faitsc%2Fankiweb/lists"}