Skip to main content

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.

TypeNon-functional
LayerData — Drift write-through cache + conflict resolution
RICER 10 × I 3 × C 80% / E 5 = 4.8 · Tier MVP-1
KPI (summary)Offline action success %; conflict rate; p95 sync latency
Traces tofeature offline-sync · C4 code · prioritization
Depends onSecurity · 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:

EventWhenKey properties
offline_action_queueduser acts while offlineactionType, idempotencyKey, queueDepth
sync_startedsync attempt beginsqueueDepth, trigger (reconnect/manual)
sync_completedsync finishes successfullyactionsApplied, durationMs, conflictsDetected
sync_conflict_resolvedconflict detected and resolvedactionType, resolution — no child PII
sync_failedsync fails after retriesreason, retryCount
idempotency_guard_fireddouble-apply blockedidempotencyKey, actionType
(audit)weeklyassert 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_completed fires with an accurate conflictsDetected count.

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_fired fires; 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.

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 consentRef exists 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 watch populates 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_resolved event 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. MemberService checks for a local consentRef before 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_writes table: (id UUID, aggregate, op, payload JSON, idempotency_key, created_at, attempt_count, status). The idempotency_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.