Offline sync — requirements
Epic — architectural gate. MVP-1 requirements for the offline-first promise (see the feature architecture). These are pass/fail launch conditions: the app must be fully usable without a network connection; writes must queue durably and sync without double-crediting. Not a screen — a substrate gate every feature depends on.
| Type | Non-functional |
| Layer | Data — Drift write-through cache + conflict resolution |
| RICE | R 10 × I 3 × C 80% / E 5 = 4.8 · Tier MVP-1 |
| KPI (summary) | Offline action success %; conflict rate; p95 sync latency |
| Traces to | feature offline-sync · C4 code · prioritization |
| Depends on | Security · Money |
Success criteria (definitive KPI)
Success = ≥99% of offline actions (chore completions, token moves, goal updates) are successfully queued and later synced; sync-conflict rate <1% of synced writes; p95 sync latency <10 s after network reconnect — measured over the first 4 weeks post-launch.
All three signals must hold simultaneously. A single week where conflict rate exceeds 1%
or p95 latency exceeds 10 s triggers a blocking investigation before the next release.
Offline action success is measured from offline_action_queued to sync_completed;
actions that fail to sync within 24 h count as failures.
Measurement & audit
Instrument the sync substrate so each KPI dimension is independently observable.
No child PII in any payload — child references are opaque consentRef-keyed identifiers:
| Event | When | Key properties |
|---|---|---|
offline_action_queued | user acts while offline | actionType, idempotencyKey, queueDepth |
sync_started | sync attempt begins | queueDepth, trigger (reconnect/manual) |
sync_completed | sync finishes successfully | actionsApplied, durationMs, conflictsDetected |
sync_conflict_resolved | conflict detected and resolved | actionType, resolution — no child PII |
sync_failed | sync fails after retries | reason, retryCount |
idempotency_guard_fired | double-apply blocked | idempotencyKey, actionType |
| (audit) | weekly | assert success ≥99%, conflict rate <1%, p95 latency <10 s |
All events feed the INTERNAL BI bucket only (no child identity, content-free, never marketing). Sync metadata is never shared with ad networks or used for child profiling.
Scope
The offline-first promise covers the data substrate, not individual screens. Every feature that reads or writes household data depends on this gate:
- In scope: Drift write-through cache; the durable pending-write queue; sync on reconnect; idempotency / no-double-credit; zero-floor enforcement offline; consent-gate enforcement in the local store; p95 sync latency and conflict-rate measurement.
- Out of scope for this epic: per-screen offline UX (owned by each feature epic); multi-device concurrent-edit arbitration; manual conflict resolution UI.
The SP3 Supabase cloud adapter and hydrate-once watch are the build surface.
Conflict resolution strategy is the primary open decision (see below).
Non-functional requirements
NFR-OFFLINE-1 — Offline-first read and act
Priority: P1 · Status: 🔨 to build Statement. Every household read and write-initiating action (chore completion, token move, goal update) succeeds without network connectivity; Drift is the primary store and requires no internet access. Acceptance
- Given the device has no network When a member completes a chore or spends tokens Then the action succeeds locally; the UI does not block or error on the network call.
- Given the app is relaunched offline When the user navigates to Today Then previously hydrated household state is available and actionable.
NFR-OFFLINE-2 — Durable write queue
Priority: P1 · Status: 🔨 to build Statement. Every write that occurs while offline (or before a sync ACK) is persisted in a durable queue within Drift; the queue survives app restart and is not lost on crash. Acceptance
- Given the device is offline and a user completes a chore When the app is force-quit and relaunched Then the pending write remains in the queue.
- Given the queue contains pending writes When the app launches Then the sync mechanism resumes processing without re-enqueuing duplicates.
NFR-OFFLINE-3 — Sync on reconnect
Priority: P1 · Status: 🔨 to build Statement. When the device regains network connectivity the SDK adapter initiates sync automatically; all queued writes are replayed to Supabase in enqueue order; the queue drains on success. Acceptance
- Given queued offline writes exist When the device reconnects Then sync starts within 5 s of reconnect detection and completes within p95 10 s for a typical household queue (<50 actions).
- Given sync completes Then local Drift state and Supabase state converge;
sync_completedfires with an accurateconflictsDetectedcount.
NFR-OFFLINE-4 — Idempotent sync / no double-credit
Priority: P1 · Status: 🔨 to build Statement. Every queued write carries a client-generated idempotency key; the Supabase adapter and service layer reject a second application of the same key; a chore completion or token credit that has already synced is never applied twice. Acceptance
- Given a write already applied to Supabase When the same write replays (e.g., after a partial sync failure) Then the server returns a no-op and
idempotency_guard_firedfires; the member's token balance is unchanged. - Given an append-only ledger move When replayed with the same idempotency key Then exactly one ledger row exists; the member balance reflects one credit, not two.
NFR-OFFLINE-5 — Zero-floor holds offline
Priority: P1 · Status: 🔨 to build Statement. The SDK service enforces the zero-floor invariant (token balance ≥ 0) locally before writing to Drift — mirroring the SQL trigger — so an offline debit that would take balance negative is rejected immediately, without waiting for sync. Acceptance
- Given a member's local Drift balance is 0 When a debit is attempted offline Then the service throws and the action is rejected; no write enters the queue; the UI surfaces an insufficient-tokens error.
- Given a queued debit that is valid locally When it syncs and the server balance is lower due to a concurrent write from another device Then the server's zero-floor trigger is the final authority; the debit is rejected at sync and a conflict event fires.
NFR-OFFLINE-6 — Consent gate holds in the local store
Priority: P1 · Status: 🔨 to build
Statement. The SDK service refuses to persist any child PII to Drift without a valid
consentRef — the same rule that governs cloud writes
(see Children's privacy NFR-COPPA-2) must hold locally so
offline operation cannot create a consent bypass.
Acceptance
- Given a parent adds a child while offline When VPC has not been captured Then the service rejects the write; no child record is written to Drift.
- Given a valid
consentRefexists in Drift When the device is offline Then child records may be written locally and queue for sync on reconnect. - Given consent is revoked while the device is offline Then the local consent record is tombstoned; subsequent child-data writes are rejected locally until sync resolves the revocation.
Architecture considerations
- Write-through cache. Drift is the authoritative read source; every SDK write hits
Drift first and is then enqueued for Supabase. Reads never touch the network; the
hydrate-once
watchpopulates Drift from Supabase on first authenticated launch. - The pending-write queue. A Drift table persists enqueued writes with their
idempotency key, enqueue timestamp, retry count, and status (
pending/syncing/failed). The queue is structurally separate from entity tables so a queue scan does not scan live data. - Idempotency. Each write is assigned a client-generated UUID at enqueue time. The Supabase adapter sends this key as a header (or upsert clause); the service layer checks before re-applying. Append-only ledger rows carry the key as a column enforced by a unique database constraint.
- Ledger replay. The append-only token ledger makes conflict resolution tractable:
moves replay in client-enqueue order; the server's zero-floor trigger is the final
arbiter. A debit rejected at sync surfaces as a
sync_conflict_resolvedevent and is communicated to the parent without exposing child PII. - Zero-floor offline. The
TokenService(SDK service layer) reads the local Drift balance before any debit write, mirroring the SQL trigger. The constraint is enforced in the service, not only in the UI, so it holds regardless of the calling surface. - RLS at sync time. Supabase RLS is never bypassed by offline operation; the adapter authenticates with the household's JWT before pushing queued writes. An expired token pauses sync until re-auth; queued writes are held, not discarded.
- Consent gate in the service layer.
MemberServicechecks for a localconsentRefbefore writing child PII to any Drift table — enforced at the service, not only in the UI, so offline operation cannot bypass it. This is substrate-level, not screen-level.
Design work (ahead of build)
- Offline / queued / sync-failed UI states. Each actionable surface (Today chores, wallet, approvals) needs an offline variant: action succeeds locally with a "queued" indicator; reconnect triggers visible sync; sync failure surfaces a retry affordance.
- Reconnect indicator. A system-level connectivity banner (
offline → syncing → synced) that appears and dismisses non-intrusively without obscuring primary content. - Pending-action treatment. A light indicator on chore cards or token actions that are queued but not yet ACK'd — communicates "this worked, it'll sync soon" to both parent and child.
- Sync-failed resolution. When a write fails permanently (zero-floor rejected at server, RLS denial) the parent is notified; the queue item is tombstoned; the local Drift state is reconciled (rollback vs. keep — design decision needed before build).
- Offline-capable first launch. Define what is available before the first hydration (empty state vs. placeholder) so onboarding does not imply a network requirement.
Decisions (resolved for MVP-1)
Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).
- D-SYNC-1 — Conflict resolution. ✅ Ledger-ordered replay (client enqueue order) for value/ledger writes; last-write-wins (server timestamp) for idempotent config/profile fields. Clock-drift handled by a server-assigned sequence on apply. Decided before the SP3.5 sync build.
- D-SYNC-2 — Sync boundary. ✅ Household-level full sync for MVP-1 (simpler, correct); per-member incremental deferred. Drives the hydrate-once
watch+ queue replay scope. - D-SYNC-3 — Approvals offline. ✅ Optimistic — a chore shows "pending approval" locally and unblocks the child; reconciles on reconnect. Neurodiversity-preferred; introduces the pending-approval state (visible to both parties).
- D-SYNC-4 — Sync trigger. ✅ Reconnect foreground only for MVP-1 (the doc's stated default); background fetch (Workmanager / iOS BG) deferred.
- D-SYNC-5 — Queue persistence schema. ✅ A Drift
pending_writestable:(id UUID, aggregate, op, payload JSON, idempotency_key, created_at, attempt_count, status). Theidempotency_key(client-generated UUID) is stored on both the queue row and, on apply, the ledger entry for dedup. Locked before the SP3.5 migration.
Out of scope (MVP-1)
- Multi-device concurrent-edit arbitration beyond last-write-wins — collaborative simultaneous edits deferred to post-MVP.
- Manual conflict resolution UI — automated resolution only in MVP-1; parent notified of failures.
- Partial / selective entity sync — full hydrate-once is the MVP-1 model.
- Background sync while the app is closed (Workmanager / background fetch) — reconnect foreground sync only.
- Optimistic offline approval flows — hard-blocked in MVP-1; deferred to the approval UX epic.
- Offline support for subscription-tier changes or account-management actions.