forked / Architecture

8. Crosscutting Concepts

Rules that apply across features and both sync modes. Failure behavior of sync is owned by Section 6.2, deployment hardening by Section 7.

8.1 Persistence and Offline

  • The local PouchDB store is the canonical working set on each device. All reads and writes go through ForkedDatabase (Section 5.1) and succeed without a network.
  • UI state flows one way: user action, ForkedDatabase write, state, render. The UI updates optimistically from the local write; replication runs in the background.
  • CouchDB, when sync is configured, holds the shared household state and revision history. An operator gateway may provision access but is never in the data path (ADR-028).
  • Offline: browsing, editing, shopping list, search and export work without a network. URL and AI import need one.

8.2 User Experience

  • Mobile-first, one React UI for browser and installed PWA. The shell adapts by width: below 768 px bottom navigation and floating actions; 768 to 1023 px an icon rail; from 1024 px a labelled sidebar. Content width is capped per page type. Touch targets are at least 44×44 px.
  • Immediate feedback: local actions confirm at once; network-dependent progress is secondary state.
  • One sync surface, one flow: Settings > Sync is the only entry point and shows the flow of the deployment’s mode, never a choice (ADR-038). Plans, devices and billing never appear in the PWA.
  • Ambient sync status: when sync is set up, the shared header shows syncing, in sync, unavailable or sign-in needed. “Syncing” stays visible about one second after replication settles. Without sync the icon is hidden.
  • Nothing leaves before the user acts: the Sync page contacts no server before the user taps Connect, Create account, Add another device or joins.
  • Interruption only for destructive confirmation such as Disconnect.
  • Visual rules live in .claude/rules/; reuse the shared app shell instead of page-local layout.

8.3 Security and Privacy

  • Local-first ownership: local-only operation requires no server. Data leaves the device only through household sync and opt-in AI import.
  • Self-hosted credentials: the CouchDB password is stored in localStorage in plain text (selfHostedLogin) because PouchDB needs it on every request and no gateway can mint tokens (ADR-038). It is sent only as Authorization: Basic with credentials: 'omit' to the announced CouchDB URL, never logged, never synced, removed on Disconnect, and the login form says it is stored. Residual risk: Section 11.
  • Cloud credentials: no password and no stored token. The device session is an opaque server-side identifier in an HttpOnly cookie; the 15-minute sync JWT lives in module memory and goes out as Authorization: Bearer with credentials omitted. Accounts sign in by magic link on the operator’s website; the PWA never sees an email address or sign-in token. A device binds to an account only after the signed-in user approves the request whose code the PWA shows. Invites are short-lived and single-use.
  • Household isolation: one CouchDB database per household. Cloud devices pin the database of their first token (Section 6.2).
  • Content Security Policy (ADR-026): script-src 'self' allows no inline or foreign scripts. connect-src allows the app origin (a same-origin CouchDB included), the announced gateway, the AI provider origins, loopback and operator-added origins (Section 7.2). The operator’s website is only opened in the system browser and needs no entry. add_header_inherit merge keeps the security headers on locations that add their own. This is also the main defence of stored secrets.
  • Announcement trust: whoever controls the serving origin controls /forked-config.json and thus where cloud requests go. Invite links cannot choose the backend: their origin or backend parameter is ignored.
  • Personal data: in cloud mode the operator holds an email address per account and device names; the PWA stores neither. Self-hosting is accountless. The PWA has no analytics or telemetry.
  • Third-party egress is opt-in (ADR-024): AI import (module src/infrastructure/ai/) sends content only to the provider the user chose, after consent asked once per provider. Keys travel in headers, are never synced, and are saved on the device or optionally locked with a passkey (WebAuthn PRF encryption at rest). Providers are configured only in AI settings; photo import stays unavailable until one is set up or when its key is reported rejected. A configured key is checked automatically against the provider’s model metadata endpoint only, which sends no user content; the cached result keeps status and a redacted error message, never the key.
  • Camera frames stay local: the QR scanner decodes getUserMedia frames on the device (native BarcodeDetector, else lazy jsqr), never stores or uploads them, and stops the stream when closed.
  • Transport: TLS, CORS and proxy configuration are deployment responsibilities (Section 7.1). Sync payloads are not end-to-end encrypted.
  • Retention: a self-hosted CouchDB keeps data until the operator deletes it. An operator may remove accounts or devices; local data on devices is untouched.

8.4 Error Handling and Recovery

  • Errors are plain language with a next action, never raw technical detail.
  • Local work is never blocked because sync or a gateway is unavailable.
  • Sync failures are recoverable state with visibility: retried where retrying can help, stopped with an instruction where it cannot. The rules per failure are in Section 6.2.
  • Logging stays under project control (one logger in core); third-party telemetry is not a dependency.

8.5 Internationalization

  • react-i18next with src/locales/en.json and src/locales/de.json; key parity is checked by the harness.
  • Language precedence: stored preference, browser language, English. The preference is stored in IndexedDB and works offline. Dates and numbers are formatted via i18next.
  • Key pattern [namespace].[context].[element]; new strings follow it. Rules: .claude/rules/.

8.6 Export and Backup

Both run fully on-device from Settings > Export & backup (ADR-032): a Nextcloud Cookbook style zip of Schema.org recipes for other managers and a versioned, lossless forked backup that restores by merging. A change to persisted document types must keep older backups restorable or bump the backup version.

8.7 App Updates

The service worker uses registerType: 'prompt' without skipWaiting, so a new version waits until the app activates it. An update found within 15 s of launch or of resuming after 30 s or more in the background is applied with a silent reload; one found during active use shows a Reload banner. The app checks on resume, on regaining connectivity and hourly, because an installed iOS PWA is suspended rather than relaunched. Performance basics: IndexedDB-backed local queries, lazy-loaded images, service worker caching of the app shell.

8.8 Testing

Each risk is tested once, at the cheapest level that can catch it.

LevelToolCovers
ArchitectureVitest (in src/test/)Layer direction and cycles
UnitVitest next to the sourcePure logic, services with stubbed fetch
StorageVitest + pouchdb-adapter-memoryForkedDatabase contracts and replica convergence, tested with two in-memory replicas
ComponentVitest + Testing LibraryPage and dialog states with mocked infrastructure, rendered through a shared test helper
SyncVitest, stubbed fetch, memory PouchDBBoth strategies: Basic header without cookies, 401/403 stop, connect check, approval, token refresh, pinning
JourneyPlaywright on the production build (vite preview)Flows needing a real browser: navigation, focus, touch, reloads, offline app shell, no third-party requests, both sync flows against stubbed servers (specs in tests/)

Journeys tagged @smoke also run on WebKit, the engine of Safari and installed iOS PWAs. This repository has no backend code; the operator’s gateway is tested in its own repository. Scenarios and their verifying checks are in Section 10.

8.9 Development Harness

CLAUDE.md holds the working rules for agents. Architecture-relevant parts of the hook-driven harness (scripts/):

  • Decisions stay with the maintainer. A pre-edit guard asks for approval of edits matching the architecture map in .claude/ (sync path, auth, API surface, persisted data, dependencies, deployment, egress, ADRs).
  • Docs follow code. The same map links code areas to the sections that describe them; an end-of-turn check blocks once when such code changed without a matching section.
  • Drift is checked mechanically. A drift check (bun run check:arc42) fails when the docs name a repository path or /api/... route that no longer exists. Known drift sits in a baseline that only shrinks.
  • The guard sees only file-edit tools (shell writes skip it), approval covers later edits of the same rule in a turn, and tests are excluded.

View source on GitHub