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,unreachableorerror. The login is saved only afterok, 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.
| Situation | Behavior |
|---|---|
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 403 | Sync 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 401 | One 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 one | syncHouseholdMismatch 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).
- Each device writes a new revision locally.
- Replication exchanges both revisions.
- 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.
- 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
| Outcome | Behavior |
|---|---|
| Denied | The PWA says so and offers “Try again”. |
| Expired | expiresAt passed or the poll answered 404/expired: “Try again”. Transient poll failures (offline, 5xx) are retried until expiry. |
| Cancelled | Polling aborts; nothing is enrolled. |
| Approved, no session | The 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.