Multiuser, web-based, Anki-compatible spaced repetition — built for classrooms and teams.
Status: pre-alpha, restarting on a new stack. An earlier TypeScript/SvelteKit implementation
reached most of Phase 1 — accounts, decks, note types, template rendering, the reviewer, .apkg
reading — before a ground-up re-evaluation moved the server to Go and dropped client-side FSRS
entirely (see docs/plans/architecture-reconsidered.md).
No Go code has been written yet. There are no users. Interfaces and storage may still change
without a migration path.
DeckShare is a self-hostable spaced repetition server and web reviewer. It imports and exports Anki deck files, uses the same FSRS scheduling algorithm, and — unlike Anki — is multiuser from the ground up: many independent accounts, shared decks with per-user progress, and a teacher/student layer on top.
The name describes what the product does: sharing decks between users. See "Why DeckShare" below — that section is a draft pending your review.
Note
DeckShare is an independent project. It is not affiliated with, endorsed by, or derived from Ankitects Pty Ltd. "Anki" is a registered trademark of Ankitects Pty Ltd and is used here only to describe file-format compatibility.
Anki is excellent and its scheduling algorithm is the best available. But it is architecturally single-user. A collection is one SQLite file where each card row carries both the content pointer and the scheduling state. There is no seam between "this deck" and "my progress on this deck."
That single design fact is why the following don't exist in the Anki ecosystem:
- A teacher assigning a deck to thirty students and seeing who is falling behind
- Two people co-authoring a deck while each keeps a private review history
- A shared deck that receives corrections upstream without blowing away your scheduling
AnkiWeb is web-based and multiuser in the sense of hosting many separate collections. It is not multiuser in the sense of users sharing anything. DeckShare targets the second meaning.
- Replacing Anki. Anki desktop remains better for power users and add-ons.
- Beating FSRS. We use FSRS as-is. Scheduling research is not our contribution.
- A better single-user desktop app. That exists.
- Sync-protocol compatibility. See below — this is a deliberate, load-bearing decision.
DeckShare speaks Anki's file format, not Anki's sync protocol.
You import .apkg and .colpkg files, including scheduling state, so an existing collection
arrives intact with review history preserved. You can export back out at any time, so nobody
is locked in. That's the whole compatibility surface.
.apkg is a zip containing a SQLite collection plus media files. File formats are not
copyrightable, so this involves no Anki code and no licensing entanglement — we write our own
reader/writer.
Sync exists to reconcile independent offline copies of a collection across devices. DeckShare is a server. You are already connected to it in order to use it, and the browser holds the only client-side state there is. The problem sync solves does not exist here. Attempting it would mean:
- Reverse-engineering an undocumented protocol with edge cases discovered only in production
- Forking a large Rust codebase and tracking its changes forever
- Inheriting AGPL obligations across the project
- Building a per-user projection layer to fake an Anki-shaped collection out of our schema
- ...to deliver a degraded experience, since none of DeckShare's actual features — classroom assignment, shared decks, progress reporting — can traverse that protocol anyway
The migration case that sync would have served is covered by one-way import. Someone brings their collection in once and is done.
What this costs us: offline study on a phone via AnkiDroid. DeckShare is a server, and studying against it needs a connection — see below.
These get conflated. They are different things and we want exactly one of them.
Network-independent grading is a Phase 1 requirement. Not for offline's sake, but for latency. Reviewing is a tight repetitive loop — show, grade, next — and a UI that blocks on a round trip is unusable on any connection worse than perfect. So the server precomputes the outcome of every possible rating for each card up front, and the client looks up the one that matches the instant you press a key, and advances. Nothing in that path waits for the network, and nothing in it runs a scheduler either — there is no FSRS implementation in the browser.
But the client's grade is an assertion, not a decision. It goes to the server, which recomputes the result independently, and the server's result is what gets stored:
- The client never waits — it looks up a precomputed answer and moves on.
- The client is never believed — it may assert which card, which rating, and when. Every number that follows is derived server-side.
That second rule is what makes the classroom layer worth building. An instructor's view of which students are struggling is a report on stored scheduling state; if a browser could write that state directly, the report would be self-assessment with extra steps. There's nothing for the server to compare against, either — the client never computed a result to submit, so a stale tab or a version skew just gets today's correct answer from the server, the same as a fresh one would.
Full offline study is not planned. Pre-caching whole decks and their media, persisting to IndexedDB, and reconciling multi-device conflicts is most of a sync implementation wearing a different hat — which is the cost this project already decided not to pay. Local grading is a latency property, not a first step toward it.
Nothing about that is a one-way door: review history is the source of truth and the server can already rebuild scheduling state by replaying it. If offline ever earns its keep, it can be built then, against a foundation that didn't compromise for it.
DeckShare schedules with FSRS (Free Spaced Repetition Scheduler), the open-source algorithm
developed by Jarrett Ye and the open-spaced-repetition
group and integrated into Anki since 23.10. We use it as-is via
go-fsrs, server-side only.
FSRS models each card with three variables (the DSR model):
- Retrievability — probability you would recall the card right now. Decays over time.
- Stability — days until retrievability falls to 90%. Your memory's half-life for that card.
- Difficulty — how resistant this card is to gaining stability.
You set a desired retention (e.g. 0.9) and FSRS schedules each card for the day its predicted retrievability crosses that threshold. Retention becomes an explicit dial, and review workload moves predictably against it.
This replaces SM-2 (1987), which uses a per-card "ease factor" multiplier and hand-tuned constants with no model of the individual's forgetting curve. FSRS instead fits its parameters to a user's actual review history, typically achieving the same retention for meaningfully fewer reviews.
Two consequences for this codebase:
review_logis training data, not an audit trail. The optimiser fits parameters from it. It is per-user, and it cannot be pruned casually.- Cold start is a real problem for classrooms. A new student has no history to fit. Ship sensible defaults and require a minimum review count (~1,000) before switching a user onto their own optimised parameters. Never fit a single parameter set across a cohort — memory behaviour is individual, and a class-wide fit is wrong for every member of it.
| Layer | Choice | Why |
|---|---|---|
| Server | Go, server-rendered HTML | Most of the app is boring CRUD — decks, note types, rosters — not something that needs a SPA framework. A small vanilla-JS island drives the reviewer only. |
| Scheduling | go-fsrs |
Server-side only — the server precomputes every rating's outcome for each card up front and ships the results down as data; the client looks one up, never computes. |
| Database | PostgreSQL + sqlc |
Row-level tenancy, real relational integrity across users/decks/progress, typed queries checked against the schema at compile time. |
| Deck I/O | Own .apkg reader/writer (modernc.org/sqlite, pure Go) |
Full control over the mapping into our schema; no native-binary-per-platform concern either way, since deployment is prebuilt Docker images. |
Why Go. A card's outcome under each of the four possible ratings depends only on its state
as of the batch fetch — a pure function, nothing session-specific — so the server can compute
all four up front and hand them down as plain data. That means no FSRS implementation needs
to run in the browser, ever, which removes the reasoning that originally picked TypeScript
end to end ("the client needs a scheduler too, so pick one language and avoid two
implementations kept in sync by hand"). Once the client doesn't need a scheduler, the server
language reopens on its own merits. Raw performance doesn't differentiate the choice — this is
a self-hosted classroom tool, not something anyone will stress that hard — but three things do:
contributor accessibility for an AGPL project that wants outside contributors (a shallower
learning curve than Rust's ownership/lifetime model), a clean subprocess boundary to fsrs-rs
if parameter optimisation ever needs it, rather than Go's cgo FFI story, and a good fit for
an app that's mostly server-rendered forms and tables, where Go's html/template auto-escapes
by default. Rust end to end and Elixir/Phoenix were both seriously considered and aren't wrong
choices either — see
docs/plans/architecture-reconsidered.md for the full
evaluation, including a direct check of the FSRS ecosystem's implementation health.
Dropping client-side FSRS also drops the client/server divergence-comparison machinery this
README used to describe: with only one implementation, there's nothing to diverge from. Rust
stays out of the stack in the MVP either way — the optimiser (the one place it could enter, as
an external fsrs-rs subprocess) is deferred out of scope for now; see the architecture doc.
This is the load-bearing decision of the project. Do not adopt Anki's schema as the primary store. Split content from per-user scheduling state:
note_types id, name, field defs, css
templates id, note_type_id, qfmt, afmt
notes id, guid, note_type_id, deck_id, fields[], tags[], created, modified
cards id, note_id, template_id, ordinal
-- content only; NO scheduling state on this row
decks id, owner_id, name
deck_access deck_id, user_id,
can_view, can_study, can_edit_content,
can_edit_settings, can_manage_access, can_delete
-- six independent per-(user, deck) permissions, not a role enum
-- the ONLY way a deck reaches a second user
user_card_state user_id, card_id, due, stability, difficulty,
state, reps, lapses, elapsed_days, scheduled_days
-- PRIMARY KEY (user_id, card_id)
review_log user_id, card_id, rating, review_time, duration_ms,
stability_before, difficulty_before
-- NOT bookkeeping: this is the optimiser's training data
user_fsrs_params user_id, deck_id NULL, fsrs_version, params JSONB,
desired_retention, optimised_at, review_count_at_fit
Warning
Store FSRS parameters as a JSON array plus an explicit version column, never a fixed-width field. The parameter count changes between algorithm versions — 17 in FSRS-4.5, 19 in FSRS-5, 21 in FSRS-6 — so a fixed column buys a schema migration every time upstream ships. The version column also lets old fitted parameters stay readable after an upgrade.
Everything multiuser follows from user_card_state being keyed on (user_id, card_id)
rather than scheduling living on cards. Shared decks, co-authoring with separate histories,
and classroom cohorts all fall out of that one choice.
Import maps an Anki collection into this shape; export flattens it back out for the importing user. Both are lossy in one direction only, and both are our code.
- Store Anki's note
guidon every note. It is what makes import and re-import idempotent. Retrofitting it means every early user's decks duplicate when they re-import. - FSRS parameters are per-user, not global. Optimised parameters are personal. A
classroom-wide parameter set is wrong for every individual in it. Optionally scope per
(user, deck)— memory behaviour differs by material. - The server decides what gets stored. A client may report which card, which rating, and when; scheduling state is derived from that, server-side, every time. This is the one that cannot be added later — state written on a client's word stays unverifiable forever, and the classroom layer is a report on exactly that state.
Milestone 1 — Single-user core. Done.
Accounts, deck CRUD, note types and templates, the reviewer, local grading against
server-derived state, .apkg import/export. A complete product for one user.
Milestone 2 — LAN multiuser, migration, LLM cards
Milestone 1 already lets several accounts share one self-hosted instance on a LAN — but only in
the AnkiWeb sense, hosting separate collections side by side (see "Why this exists" above).
Nothing yet lets one of those users share a deck with another: this milestone builds
deck_access grant/revoke so co-authoring a deck while each author keeps a private review
history actually works; tightens .apkg/.colpkg import/export fidelity against desktop Anki
collections (a couple of known scheduling-state gaps on import, plus full-collection .colpkg
export); and adds LLM-generated cards via the documented paste-in text format described below.
Milestone 3 — Classroom The layer on top of Milestone 2's sharing mechanism: instructor assigns a deck to a cohort, sees per-student retention, due counts, and lapse hotspots.
Explicitly not doing Each of these is a decision rather than a backlog item:
- Anki sync protocol — see above.
- Full offline study (deck + media pre-caching, IndexedDB, multi-device conflict resolution). DeckShare is a server. Local grading is a latency property, not a step toward this.
- Deck forking and a public deck directory.
deck_accessalready covers co-authoring and the classroom, and anyone wanting an outside deck can import its.apkg. Worth being precise: export-then-reimport is not forking — reimport creates new cards, and your progress is keyed to the old ones, so it doesn't come with you. - Native mobile apps (the web app is the mobile story).
- Plugin system.
- LLM-generated cards, as a feature that calls a model API on your behalf. A documented paste-in text format, filled by whatever model you already use, is fair game — no API key, no per-token cost, no third party in your study data.
AGPL-3.0-or-later.
Because DeckShare contains no Anki-derived code, we inherit no licence — Anki is AGPLv3-or-later, and forking its sync server would have made AGPL permanent and irreversible for this project (hundreds of contributors, no CLA, nobody able to grant an exception). Dropping sync means that constraint simply doesn't apply, which is why the choice was ours to make freely rather than inherited.
We chose AGPLv3-or-later anyway: it's consistent with the ecosystem DeckShare sits in, and it prevents someone running a closed proprietary fork as a hosted service. The trade-off is that some organisations have blanket policies against AGPL, which can limit institutional adoption — a real consideration for something aimed at schools, and a cost we accepted knowingly rather than one we missed.
Deck content is a separate matter from code, and DeckShare never redistributes any. Publicly
shared decks carry their own licence terms, and redistributing them without permission is
something Ankitects has publicly objected to — so there is no deck catalogue, no directory,
and no republication. A deck reaches a second person through a deck_access row on the
instance it already lives on, or through a file its owner passed along. Should any
deck-sharing surface ever cross instances, per-deck licence metadata becomes a prerequisite,
not an afterthought.
Not legal advice.
ANKI is a registered trademark of Ankitects Pty Ltd, and it is actively enforced —
Anki Pro was compelled to rebrand to Noji in June 2025, and Ankitects maintains a public
"Anki knockoffs" page naming apps it
considers to be trading on the brand.
The distinction that matters is descriptive use versus brand use:
- ✅ "DeckShare imports Anki decks" — nominative fair use. Standard, low risk.
⚠️ A product brandedAnkiMultiuserwith a domain and marketing — this is what got Anki Pro renamed. Being FOSS reduces the commercial-confusion argument but does not eliminate it.- ❌ Anything containing
AnkiWeb— the name of the official service. Direct implication of affiliation. Avoid entirely.
Community repos using an anki- prefix as a plain descriptor (anki-sync-server-rs,
anki-connect, the ankicommunity org) have not been targeted, because they are obviously
tools rather than competing products. DeckShare is a competing product, which puts it closer to
the risky end. Hence a distinct name, with compatibility carried in the description and
topics — GitHub indexes both heavily, and the anki topic page is where the target audience
browses. Findability does not require the name.
Repo: deckshare
Description: Multiuser, web-based spaced repetition for classrooms and teams.
FSRS scheduling, Anki deck import/export.
Topics: anki, spaced-repetition, fsrs, flashcards, srs, self-hosted,
education, classroom, go
Warning
DRAFT — placeholder pending your review. This replaces the old "Why DeckShare, and what was rejected" section (see git history / docs/plans/rename-enshu-to-deckshare.md for the original). The naming rationale is a statement of intent, not a mechanical fact, so it needs a real writeup from you rather than an invented one — this stands in until then.
DeckShare is a plain, descriptive name for what the product does: sharing decks between users.
It trades the wordplay of the old name for straightforward searchability, and sidesteps the
"is this an Anki add-on?" ambiguity that deckshare (opaque to non-Japanese speakers) didn't fully
avoid either.
Naming collision check (informal — see docs/plans/rename-enshu-to-deckshare.md §1): the only notable prior use is Alfresco DeckShare, a dormant ~2011 presentation-hosting plugin, different domain, no active trademark found. Do a proper USPTO/domain check before this name is load-bearing (before a real launch, domain purchase, etc.) — the same caution the old section gave itself.
- ankitects/anki — the real thing. Rust core, Python/TS bindings.
- AnkiWeb — official hosting. Many collections, no sharing between them.
- open-spaced-repetition — FSRS reference implementations.
- awesome-fsrs — implementation index.
- anki-apkg-parser,
anki-reader — prior art for
.apkgparsing in Node.
Nothing to contribute to yet. Licence will be settled before the first outside contribution.