forked / Architecture

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).

PathResponsibility
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/.

BlockResponsibility
Replication coreLive, retrying, bidirectional replication used by both strategies, plus the sync status.
Self-hosted strategyRemote <couchdbUrl>/forked; adds Authorization: Basic and omits browser credentials; 401/403 stops sync. Performs the connect check (Section 6.1).
Gateway strategyIn-memory token holder with refresh, Bearer authentication, database pinning, revoked-session handling (Section 6.2).
Device approvalRequests an approval, polls (abortable) until approved, denied or expired, then verifies the session, enrolls and starts sync.
InvitesCreates, parses and accepts invites by link, QR payload or pairing code.
Dev auto-connectDev 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/.

KeyContent
selfHostedLoginSelfHostedLogin {username, password}. The password is stored on purpose (Section 8.3). The database name is the constant forked.
gatewayEnrollmentGatewayEnrollment {database?, syncHouseholdMismatch?, syncSessionRevoked?}. Its presence means “enrolled”. The pinned database is set from the first sync token. The gateway URL is not stored.
runtimeAnnouncementThe 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
EntityFields and invariants
UserSettingsOne 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.
Categoryid 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.
IngredientShared 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.
Recipeservings, 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.
ShoppingListItemingredientId 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.

View source on GitHub