ELTIGA.

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

  1. 01

    Classic Leitner schedule: boxes come due every 1, 2, 4, 8 and 16 days, with a mastered tray.

  2. 02

    Phones and tablets get a portrait desk, tap to flip and swipe to grade; desktops get keyboard shortcuts and one-step undo.

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

  4. 04

    Installable PWA that works fully offline, with a text mode for browsers without WebGL.

  5. 05

    Optional sign-in with Google or a 6-digit email code syncs decks between devices; guest data never leaves the browser.

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

Lietner architectureThree rows. First, the device, which is the source of truth: a DOM overlay drives a three.js desk rendered on demand, pure Leitner rules schedule cards on integer calendar days, and IndexedDB stores every deck and card with tombstones and dirty flags. Second, optional sync for signed-in accounts: the sync engine pushes dirty rows and pulls by a server-stamped cursor, Supabase Postgres enforces row-level security per account and SQL triggers cap timestamps at the server clock, and the auth SDK is only loaded when someone signs in. Third, offline and privacy: a service worker precaches the app so guest mode works offline, and telemetry is cookieless page views plus crash reports with card text scrubbed.Device · source of truthDOM overlayno UI framework; text mode withoutWebGLthree.js deskrenders on demand; GSAP cardanimationsLeitner rulespure functions; 1, 2, 4, 8, 16 dayboxesIndexedDBtombstones, dirty flags, monotoniceditspush dirty rows · pull by server_atOptional sync · signed-in accountsSync enginedebounced, on focus and reconnect; onetab at a timeSupabase Postgresrow-level security; triggers cap timeat the server clockAuthemail code or Google; SDK loads onlyon sign-inOffline and privacyService workerprecached app shell; guest mode worksofflineTelemetrycookieless page views; card textscrubbed from crash reports
Mechanism: the device owns the data. The server is an optional per-account mirror, and conflicts resolve per row with the server's clock as the ceiling.

§ 06

Engineering decisions

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

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

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

  4. D-04

    Isolate accounts with Postgres row-level security, covered by pgTAP tests that run in CI against a local Supabase stack.

  5. D-05

    Render the three.js scene on demand, cap the pixel ratio on phones and drop quality automatically when frames run slow.

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

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

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