5. Building Block View
5.1 Layers and Code Map
The frontend has five layers. Dependencies point downward only: app > features > shared > infrastructure > core (ADR-031). An architecture test in src/test/ checks every layer pair and the absence of cycles. Imports between feature modules are not restricted (Section 11).
| Path | Responsibility |
|---|---|
| src/app/ | Router, root layout (bootstrap, runtime config, invite deep links), i18n setup |
| src/features/recipes/ | Recipe list, editor, import (JSON file, AI photo import) |
| src/features/shopping/ | Shopping list |
| src/features/settings/ | General (language, export and backup), ingredients and categories, AI, sync |
| src/shared/ | Cross-feature components (app shell, modals, sync indicator), hooks, utils |
| src/infrastructure/database/ | ForkedDatabase, the only persistence API; bootstrap and default seeding |
| src/infrastructure/storage/ | PouchDB instance and mode dispatch, local records, runtime announcement, sync status store |
| src/infrastructure/sync/ | Sync strategies, device approval, invites |
| src/infrastructure/auth/ | Gateway session client (cloud mode) |
| src/infrastructure/ai/ | AI providers, consent, credentials, connection check (ADR-024) |
| src/core/ | Persisted domain types and logger; no React, no I/O, no imports from other layers |
| src/locales/ | en.json, de.json |
Path alias @/* maps to src/*.
5.2 Sync Building Blocks
The storage module in src/infrastructure/storage/ owns the PouchDB instance, the sync status store and the start and stop of replication. Starting sync dispatches on the deployment’s mode, never on stored data: with a gateway announced it starts the gateway strategy for an enrolled device, otherwise the self-hosted strategy for a stored login. A stored record of the other mode is ignored, not migrated; reconnecting restores sync and local data is untouched. Sync modules are in src/infrastructure/sync/.
| Block | Responsibility |
|---|---|
| Replication core | Live, retrying, bidirectional replication used by both strategies, plus the sync status. |
| Self-hosted strategy | Remote <couchdbUrl>/forked; adds Authorization: Basic and omits browser credentials; 401/403 stops sync. Performs the connect check (Section 6.1). |
| Gateway strategy | In-memory token holder with refresh, Bearer authentication, database pinning, revoked-session handling (Section 6.2). |
| Device approval | Requests an approval, polls (abortable) until approved, denied or expired, then verifies the session, enrolls and starts sync. |
| Invites | Creates, parses and accepts invites by link, QR payload or pairing code. |
| Dev auto-connect | Dev server builds only: stores the dev CouchDB’s throwaway login. No-op in production builds and under browser automation. |
Runtime announcement. At startup the PWA loads GET /forked-config.json (1.5 s timeout) and parses {gatewayUrl, couchdbUrl} (Section 7.2). A cached copy in localStorage applies immediately, so an offline start knows its mode; the fetch refreshes it. A 404, a non-JSON body or an invalid value clears the cache; offline or failing requests keep it. A gateway is offered if and only if the announcement names one; the CouchDB address is the announced URL (relative paths resolve against the app origin) or <origin>/couchdb. There is no built-in gateway; development builds may set VITE_FORKED_GATEWAY_URL.
Sync page. Settings > Sync renders exactly one section, chosen by whether a gateway is offered: the self-hosted section (login form, status, Disconnect) or the cloud section (create account, join with scanner or code, add device, manage account, Disconnect). The shared header shows the sync status.
Operator gateway. The gateway and website are not building blocks of this repository. The PWA contains only their client; the contract is Section 7.3.
5.3 Local Records
Stored in localStorage, never synced; the types are in src/core/types/.
| Key | Content |
|---|---|
selfHostedLogin | SelfHostedLogin {username, password}. The password is stored on purpose (Section 8.3). The database name is the constant forked. |
gatewayEnrollment | GatewayEnrollment {database?, syncHouseholdMismatch?, syncSessionRevoked?}. Its presence means “enrolled”. The pinned database is set from the first sync token. The gateway URL is not stored. |
runtimeAnnouncement | The cached announcement. |
5.4 Data Model
Persisted data are PouchDB documents in IndexedDB that replicate unchanged to CouchDB (ADR-028). Each is a ForkedDocument (module src/infrastructure/database/): _id, _rev, a type, an optional default flag for seeded documents, and the entity fields in content. Documents already replicate to other devices, so a schema change is a compatibility decision and must keep older backups restorable (Section 8.6). Types live in src/core/types/.
classDiagram
class UserSettings {
<<type settings, id settings>>
locale
initializedDefaultsVersion?
}
class Category {
<<type categories>>
id, name, icon, color, order, food
}
class Ingredient {
<<type ingredients>>
id, name, categoryId
defaultIngredient?, usuallyAtHome?
}
class Recipe {
<<type recipes>>
id, title, servings, ingredients, steps
prepTimeMinutes?, cookTimeMinutes?
createdAt, updatedAt, plannedAt
}
class ShoppingListItem {
<<type shopping-list>>
id, ingredientId, amount?, checkedAt
}
Category "1" --> "*" Ingredient : categorizes
Ingredient "1" --> "*" ShoppingListItem : referenced by
Ingredient "1" --> "*" Recipe : used in
| Entity | Fields and invariants |
|---|---|
UserSettings | One document with _id settings. locale is de or en. initializedDefaultsVersion (<locale>_<seed version>) records the last applied default seed. Device preferences; not restored from a backup. |
Category | id is a stable slug. icon is a Lucide icon name, color a Tailwind color name such as blue-500, order the display order. food is false for non-food categories, whose ingredients are hidden from recipe ingredient search and the recipe editor; unknown categories count as food (ADR-030). Fallback category UNKNOWN_CATEGORY. |
Ingredient | Shared pool for recipes and shopping list (food and household items). categoryId references a category. defaultIngredient marks seeded entries. usuallyAtHome marks staples that are not preselected when adding recipe ingredients to the shopping list. Optional flags are omitted when false. An ingredient can be deleted only while no recipe or shopping item (active or history) references it. |
Recipe | servings, optional prepTimeMinutes and cookTimeMinutes, ingredients (ingredientId, optional free-form amount) and steps (instruction, ingredientIds of the ingredients a step uses). createdAt and updatedAt are ms timestamps; plannedAt is the planned cooking time, 0 if not planned. |
ShoppingListItem | ingredientId references an ingredient; amount is free-form text such as 200 g; checkedAt is 0 while on the list, otherwise the UTC ms timestamp of checking off. Adding an ingredient that is already checked off reactivates that entry, so an ingredient has at most one entry. |
Default categories, ingredients and recipes are seeded per locale from public/defaults/ at bootstrap; a bumped seed version reseeds existing installs.