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,
ForkedDatabasewrite, 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 asAuthorization: Basicwithcredentials: '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
HttpOnlycookie; the 15-minute sync JWT lives in module memory and goes out asAuthorization: Bearerwith 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-srcallows 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 mergekeeps 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.jsonand thus where cloud requests go. Invite links cannot choose the backend: their origin orbackendparameter 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
getUserMediaframes on the device (nativeBarcodeDetector, else lazyjsqr), 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-i18nextwithsrc/locales/en.jsonandsrc/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.
| Level | Tool | Covers |
|---|---|---|
| Architecture | Vitest (in src/test/) | Layer direction and cycles |
| Unit | Vitest next to the source | Pure logic, services with stubbed fetch |
| Storage | Vitest + pouchdb-adapter-memory | ForkedDatabase contracts and replica convergence, tested with two in-memory replicas |
| Component | Vitest + Testing Library | Page and dialog states with mocked infrastructure, rendered through a shared test helper |
| Sync | Vitest, stubbed fetch, memory PouchDB | Both strategies: Basic header without cookies, 401/403 stop, connect check, approval, token refresh, pinning |
| Journey | Playwright 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.