Cross-platform registry seams (why the poly-repo exists)
Parent: key-designs
@lucerna/core never imports a shell, yet drives both a Next.js web app and an Electron desktop app. The mechanism: three module-level registry seams, each set once at boot by each shell, reached through a lazy proxy so import order doesn't matter.
Seam 1 — the platform adapter (core/src/platform/index.ts). PlatformAdapter = storage (kv) + navigate(path) + origin + setTheme. buildDefaultAdapter() is browser-aware (wires localStorage/location/documentElement if window exists, no-ops under SSR/tests so module-init reads don't crash). setPlatformAdapter(impl) swaps it; platform is a getter-proxy that re-reads the active adapter per access.
Seam 2 — repository dependency-inversion (the important one). Interfaces live in core/src/domain/repositories/; the barrel is the DI container: setRepositories(partial) merges impls into a registered map, resolve(key) throws "core: ${key}Repository not registered" (fail-loud, no fallback), and lazy<T>(key) returns a Proxy that resolves the active impl and binds methods to it on each call. Core does getVocabularyRepository().create(...) and never knows whether it hit HTTP or SQLite. (Honest debt: HttpError is re-exported from the domain barrel because usecases branch on err.status === 401 — the clean fix is acknowledged as unshipped.)
Seam 3 — zustand stores in the domain package (core/src/state/). UI-agnostic stores persist through platform.storage (not localStorage directly), so the same store works under Electron, SSR, and tests; React shells consume them as ordinary hooks.
One repository, two impls. VocabularyRepository (core interface) →
- web:
HttpVocabularyRepository— a pure delegate tovocabularyApi.*, no local state; - desktop:
HybridVocabularyRepository— local-SQLite + outbox wrapper (offline-first-outbox). Each shell'scoreBootstrap.tscomposes its bundle and callssetRepositories(...)once (web → 10 HTTP delegates; desktop → 7 hybrid + 3 HTTP).
What actually differs, web vs desktop (beyond storage): auth transport (web session cookie vs desktop Bearer token, because the webview drops cross-site Set-Cookie); navigation (web full location.href reload vs desktop SPA pushState to preserve the sqlite handle/outbox across auth events); locale (web URL-prefixed /en/… vs desktop store-only); and the Electron main process (better-sqlite3, OS keystore, auto-updater, local TTS) that the browser simply has no equivalent for.
This is why the whole poly-repo exists: the seams are the contract that lets one domain core serve fundamentally different runtimes.