Parent: lucerna · Repo root:
~/Develop/projects/Otium/
Source of truthCode is authoritative;
ops/ARCHITECTURE.md(2026-05-23) has drifted on almost every infra claim — all confirmed against code:
- Desktop is Electron, not "Tauri 2" (
desktop/package.jsonelectron 33 +electron-builder+better-sqlite3;main/*import from 'electron'). A Tauri→Electron migration happened after the doc; manywa-sqlite/Tauri comments in desktop code are stale copy-paste.- Desktop local store is native
better-sqlite3via IPC, not wa-sqlite (the doc's "wa-sqlite cache" is only true for web). The schema block isFULL_SCHEMA_SQLreapplied every boot; thePRAGMA user_versionmigration framework survives only on web.- Bearer-token auth is shipped on desktop, not "planned" (
setSessionProvider,Authorization: Bearer).- Still aspirational: the
libraryhybrid slice (quota/local-move), real per-user scoping (every desktop row uses the literal'local'user + wipe-on-session-change), and the advertisedbrowser-plugin/mobileshells (do not exist).landing/is an empty stale clone; the real site isLandingPage/(Payload CMS). Identity is dual/in-flight (Appwrite service + better-auth tables both present).
Stack (verified)
- Domain core: framework-agnostic TS (
@lucerna/core, peer-dep onlyzustand,tsup). - Shared UI:
@lucerna/design-system(React 19, Radix, CVA, Tailwind 4). - Shells: Next.js 15 (web) · Electron + Vite (desktop) · Next.js 15 + Payload CMS 3 (landing).
- Backends: Go 1.26 + echo (
auth= Appwrite-backed;mainbackend= domain API +pgx+google/wire, owns the SRS) · Python + FastAPI + spaCy (nlp-api, DE+EN). - Data: Postgres; client SQLite (
wa-sqliteweb /better-sqlite3desktop);flexsearch. Cohesion via a private Verdaccio registry; Coolify deploy; R2 assets.
The reading pipeline (the spine)
Ingest is 100% client-side; the server stores the parsed result. The load-bearing idea is the content-keyed, two-coordinate sentence.
flowchart LR UP["upload EPUB/PDF/txt"] --> P["parse (client) core/src/upload/*"] P --> H["contentHash (MD5) = join key for vocab/notes/cache"] P --> SEG["sentence split Intl.Segmenter"] SEG --> CS["CachedSentence raw coords + display coords + displayToRaw[]"] CS --> PG["Page (transient) built from same-pageNumber slice"] PG --> READ["read · tap word · save · underline · review"] H --> POST["POST /api/books (create)"]
- Parse (
core/src/upload/parseEpub|parsePdf|parseText): EPUB via JSZip → OPF/spine → HTML-stripped chapters joined; TOC from NCX/EPUB3-nav with cumulative char offsets; rejects scanned/thin books. contentHash(upload/contentHash.ts, MD5, deliberately legacy-compatible) is the join key — vocab/notes/sentence-cache all anchor to it, so re-upload relinks.CachedSentence(persisted unit, key(contentHash, sentenceIdx)) carries two coordinate systems — raw source offsets and display offsets the user reads — plusdisplayToRaw[]. This is the backbone: see raw-display-cursor.Pageis a transient runtime aggregate (only the current page in RAM); layout decides which sentences share apageNumber— see dynamic-pagination (DOM-measured, reflows on font/viewport).- On top of the same sentence stream: full-text-search (per-book FlexSearch, disk-persisted) and, on desktop, tts-audiobook (local Piper audiobook with raw-offset-anchored karaoke). Ingestion that feeds all of this: upload-ingestion.
Reading domain model (plain terms)
Book (full text + ISBN provenance) → split into CachedSentences → read page by page (Page, transient) → position/marks stored content-anchored, not geometry-anchored so they survive font/layout changes: Anchor (current position = ≤200-char sentence prefix), Bookmark, ReadingNote (highlight with book-absolute raw offsets), VocabularyEntry (lemma + all inflected forms[] + POS/VerbTable/NounInfo + context + SRS state). POSCategory (10-way taxonomy, colors, 11-language labels, alias folding) and VerbTable (conjugation grid) are the linguistic value-objects.
Poly-repo shape
No monorepo tool — ~12 sibling repos cohere via the Verdaccio registry.
| repo | role | stack |
|---|---|---|
core | @lucerna/core domain core | TS, tsup |
design-system | shared UI atoms + tokens | TS/React 19 |
web | web shell (online-first + read caches) | Next.js 15, wa-sqlite |
desktop | desktop shell (offline-first) | Electron, better-sqlite3, sherpa-onnx |
LandingPage | marketing site | Next.js 15 + Payload CMS 3 |
auth | identity (Appwrite-backed) | Go + echo |
mainbackend | domain API, owns SRS | Go + echo + pgx + wire |
nlp-api | spaCy/dep-parse/translate/OCR | Python + FastAPI |
e2e · ops · blog · landing(empty) | tests · infra · content · — | — |
Key designs (the pillars)
- key-designs — the full index. Reading substance: dual-coordinate backbone, dynamic-pagination, all-forms-underline, vocabulary-loop, full-text-search, tts-audiobook. Linguistic core: linguistic-two-engines, upload-ingestion. Architecture pillars: cross-platform-seams, offline-first-outbox, backends-three-services.