case file 03Author
Lietner
Leitner-system flashcards on a 3D desk. Free, open source, works offline, with optional sync.
- Role
- Author
- Stack
- 9 components
- Status
- Live at lietner.app · open source under the MIT license
§ 01
The problem
Plain flashcard apps feel flat. Lietner makes the Leitner system visible: cards physically move between boxes, so progress shows on the desk. It is built for students studying on their own, mostly on phones, who should be able to start a deck on a laptop, review on the bus and lose nothing, with or without an account.
§ 02
The build
Five wooden boxes sit on a three.js desk. Each due card lifts out, flips to show its answer and files itself into the next box, or back into box 1 if you missed it. The same desk works on phones, tablets and desktops, fully offline as a guest; signing in syncs decks and progress between devices.
result
Live at lietner.app with public source on GitHub. CI runs lint, typecheck, unit tests and a build; pgTAP row-level-security tests and sync integration tests against a local Supabase stack; and Playwright end-to-end tests on desktop Chrome, an emulated iPhone and iPad, and a browser with WebGL disabled. No user numbers are claimed.
§ 03
What it does
- 01
Classic Leitner schedule: boxes come due every 1, 2, 4, 8 and 16 days, with a mastered tray.
- 02
Phones and tablets get a portrait desk, tap to flip and swipe to grade; desktops get keyboard shortcuts and one-step undo.
- 03
Cards in any language or script, including right-to-left. Import CSV/TSV (including Anki's export) or paste JSON cards written by your own AI.
- 04
Installable PWA that works fully offline, with a text mode for browsers without WebGL.
- 05
Optional sign-in with Google or a 6-digit email code syncs decks between devices; guest data never leaves the browser.
- 06
JSON export and import of everything, and account deletion from Settings.
§ 04
Visit Lietner
The production site, live. Opens in a new tab
§ 05
Architecture
Drawn from the project's own documentation
§ 06
Engineering decisions
- D-01
Make IndexedDB on the device the source of truth, and the server a per-account mirror. Guests never download the Supabase SDK; it is loaded with import() only when someone signs in.
- D-02
Sync with last write wins per row, enforced by SQL triggers: updated_at is capped at the server clock so a device with a fast clock can't win every conflict, and deletions are final tombstones.
- D-03
Pull by a server-stamped server_at cursor with keyset paging and a 60-second lookback, so an offline edit pushed days later is still picked up by every device.
- D-04
Isolate accounts with Postgres row-level security, covered by pgTAP tests that run in CI against a local Supabase stack.
- D-05
Render the three.js scene on demand, cap the pixel ratio on phones and drop quality automatically when frames run slow.
- D-06
Keep the UI behind DeskView and CardView interfaces, so a plain-DOM text mode for browsers without WebGL runs the identical review flow.
- D-07
Never let card text reach telemetry: page views are cookieless, and Sentry events and breadcrumbs pass through a scrubber with its own tests.
- D-08
Keep scheduling as pure functions over integer calendar days, so time zones and daylight-saving shifts never move a card by a day.
§ 07
Secured and shipped
- [pass]Row-level security limits every account to its own rows; pgTAP tests prove the isolation in CI.
- [pass]SQL triggers cap client timestamps at the server clock and make deletions final.
- [pass]Telemetry is cookieless page views plus Sentry crash reports with card text scrubbed out; no ads, no data sales.
- [pass]Synced data lives in the EU (Supabase, Ireland); grade history never leaves the device.
- [pass]Signing out flushes unsynced changes first, then wipes account data from the device.
stack
TypeScript / Vite / three.js / GSAP / IndexedDB / Supabase / Workbox PWA / Vitest / Playwright
open channel
Have a problem shaped like this one?
Tell me what your team is stuck on and what you need built.
