2026-09-23·by Sijie Wang#lucerna#software#architecture

cross-platform-seams

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 to vocabularyApi.*, no local state;
  • desktop: HybridVocabularyRepository — local-SQLite + outbox wrapper (offline-first-outbox). Each shell's coreBootstrap.ts composes its bundle and calls setRepositories(...) 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.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →