forked / Architecture

6. Runtime View

Runtime scenarios and the rules that govern sync failures. The deployment fixes the sync mode (ADR-038): self-hosted (Section 6.1) unless /forked-config.json announces a gateway, then forked cloud (Section 6.4). Both replicate directly with CouchDB (ADR-028). Participants and modules are in Section 5.

6.1 Connect a Device (Self-Hosted Mode)

A device signs in from Settings > Sync with a user name and password. The CouchDB address is the announced couchdbUrl and the database is always forked. Nothing is sent before the user taps Connect.

sequenceDiagram
  actor User as Household member
  participant PWA as Forked PWA
  participant CDB as CouchDB (behind the proxy)

  User->>PWA: Enter user name and password, Connect
  PWA->>CDB: GET /forked (Authorization: Basic, credentials omitted)
  alt 200
    PWA->>PWA: Store SelfHostedLogin
    PWA->>CDB: Live replication (push and pull)
    PWA-->>User: Connected
  else 401 or 403
    PWA-->>User: Sign-in failed, nothing saved
  else 404
    PWA-->>User: Database not found, nothing saved
  else network error or timeout
    PWA-->>User: Server unreachable, nothing saved
  end
  • The check result is ok, unauthorized, not-found, unreachable or error. The login is saved only after ok, so a wrong password never replaces a working login.
  • All devices share one CouchDB user. A further device repeats this flow with the same credentials; there is no invite, pairing code or device record.
  • Local data of a new device uploads on its first sync and existing data downloads; conflicts follow Section 6.3.
  • Revoking one device is not possible. Changing the password locks out every device, which then show the auth-failed state of Section 6.2.
  • Dev builds store a throwaway login automatically (Section 5.2).

6.2 Local Edit, Offline and Replication

Local work never waits for the network. Every write goes to the local PouchDB first; live replication (Section 5.2) carries it to CouchDB when reachable. There is no application-level sync queue.

sequenceDiagram
  actor User
  participant PWA as Forked PWA
  participant LocalDB as PouchDB / IndexedDB
  participant GW as Operator gateway (cloud only)
  participant CDB as CouchDB

  User->>PWA: Create or edit
  PWA->>LocalDB: Persist immediately
  PWA-->>User: Confirmed locally

  alt Network unavailable
    PWA->>PWA: Sync stays paused, retried (rules below)
  else Network available
    opt Cloud: no token, token expires within 2 minutes, or CouchDB answered 401
      PWA->>GW: POST /api/devices/sync-token (session cookie)
      GW-->>PWA: 15-minute JWT, or 401 (session revoked)
    end
    opt Cloud: token names another database than the pinned one
      PWA->>PWA: Mark household mismatch, stop sync
    end
    PWA->>CDB: Replicate (Basic auth or Bearer JWT)
    CDB-->>PWA: Acknowledge, or 401/403 in self-hosted mode (auth failed)
  end

Failure rules

This section owns the failure behavior; other sections link here.

SituationBehavior
Sync cannot start (offline start, timeout, 5xx, unreachable)Status error. The PWA retries on the browser online event and every 30 s while the status is error. Once replication runs, PouchDB’s own back-off retry takes over.
Self-hosted: CouchDB answers 401 or 403Sync stops, the in-memory syncAuthFailed flag is set, no retry (it would only feed CouchDB’s lockout). The header shows “Sign-in needed” and the Sync page offers to update the login. A successful connect clears it.
Cloud: token request or refresh fails (5xx, timeout, 404)Status error, same retry as above.
Cloud: CouchDB answers 401One token refresh and one retry.
Cloud: sync-token endpoint answers 401 (device removed, session expired)syncSessionRevoked is set. Sync stops and no further tokens are requested. The Sync page shows “This device was disconnected” with Create account and Join.
Cloud: token names another database than the pinned onesyncHouseholdMismatch is set. Sync stops with status error; the Sync page warns and offers Disconnect.

The two cloud flags live in the local GatewayEnrollment; a new enrollment replaces it and clears them. The cloud JWT lives in module memory only, refreshed on demand when less than 2 minutes remain; concurrent requests share one refresh. Gateway sessions slide: a successful GET /api/auth/session or POST /api/devices/sync-token extends a session with less than 29 days left to 30 days, so a regularly syncing device never has to rejoin.

6.3 Conflicts

Concurrent offline edits on two devices converge by CouchDB’s deterministic revision winner (ADR-025).

  1. Each device writes a new revision locally.
  2. Replication exchanges both revisions.
  3. Every replica picks the same winner from the revision tree (longest history, then highest revision hash). Timestamps play no role, so an older concurrent edit can win.
  4. Losing revisions stay as conflict leaves; the application neither shows nor resolves them.

6.4 Connect Through an Operator Gateway

Applies only when the serving origin announces a gateway. The entry point is Settings > Sync; there is no first-run screen. An unconnected page offers Create account and Join with QR or code. A connected page offers Add another device, Manage account (opens <gatewayUrl>/account in the system browser) and Disconnect. Invite deep links open the Sync page with the link prefilled. Nothing is sent to the gateway before the user starts one of these actions; an enrolled device checks its session when the page opens. Routes and website pages are in Section 7.3.

6.4.1 Create an Account (Device Approval)

sequenceDiagram
  actor User
  participant PWA as Forked PWA
  participant API as Operator gateway
  participant Web as Operator website (system browser)

  User->>PWA: Create account
  PWA->>API: POST /api/devices/approval {deviceName}
  API-->>PWA: requestId, code, verificationUrl, expiresAt, intervalSeconds
  PWA-->>User: Show code, open the approval page
  User->>Web: Open verificationUrl, sign in by magic link
  Web-->>User: Same code; user taps "Connect this device"
  loop every intervalSeconds until expiresAt
    PWA->>API: GET /api/devices/approval/{requestId}
    API-->>PWA: pending
  end
  API-->>PWA: approved + session cookie
  PWA->>API: GET /api/auth/session
  API-->>PWA: authenticated
  PWA->>PWA: Store GatewayEnrollment, start sync
OutcomeBehavior
DeniedThe PWA says so and offers “Try again”.
ExpiredexpiresAt passed or the poll answered 404/expired: “Try again”. Transient poll failures (offline, 5xx) are retried until expiry.
CancelledPolling aborts; nothing is enrolled.
Approved, no sessionThe session check fails: the PWA reports an error and enrolls nothing.

The code lets the user confirm on the website that the request is theirs. The PWA polls instead of receiving a redirect because on iPhone a link opened outside the installed PWA lands in Safari with separate storage (ADR-038).

6.4.2 Join with QR or Pairing Code, Add Another Device

sequenceDiagram
  actor Member as Enrolled device
  actor NewDevice as New device
  participant API as Operator gateway

  Member->>API: POST /api/devices/invite
  API-->>Member: link, QR payload, pairing code, expiry
  Member-->>NewDevice: Share QR, code or link
  alt Inside the installed app
    NewDevice->>NewDevice: Scan QR live or type pairing code
  else Outside the app
    NewDevice->>NewDevice: Open link (Sync page opens prefilled)
  end
  NewDevice->>API: POST /api/devices/accept-invite {code}
  API-->>NewDevice: Session cookie, sync starts

The invite panel shows QR, pairing code, copy button and expiry, and refreshes before expiry. The live scanner decodes camera frames on the device only. Where no camera is available or permitted, the scanner says so and the pairing code is the path. Invite links and codes carry no backend address: the gateway is always the announced one.

6.4.3 Disconnect

Disconnect asks the gateway to remove this device (best effort; failures are logged), stops sync and clears the enrollment. Local recipes stay. A device removed on the website is handled by the revoked-session rule in Section 6.2; after reconnecting it re-uploads its local data.

View source on GitHub