Skip to content

Repository files navigation

DeckShare

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.


Why this exists

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.

Non-goals

  • 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.

Compatibility model

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.

Why not implement sync

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.

Offline vs. network-independent

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.


Scheduling: FSRS

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_log is 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.

Architecture

Stack

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.

The schema decision

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.

Three things to get right from day one

  • Store Anki's note guid on 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.

Roadmap

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_access already 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.

Licensing

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.


Naming and trademark

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 branded AnkiMultiuser with 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

Why DeckShare

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.


Prior art


Contributing

Nothing to contribute to yet. Licence will be settled before the first outside contribution.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages