Summary
Make TaskNexus truly mobile-proof by adding an offline-first sync layer: tasks are stored locally, edits apply instantly (optimistic UI), and a background queue syncs deltas to Supabase. Handle conflicts deterministically, show a tiny connectivity badge, and keep everything resilient to flakey networks.
Goals
- Local first: read/write from a local DB; app feels instant with no spinner.
- Optimistic updates with rollback on server rejection.
- Background sync: flush pending ops when connectivity/auth returns.
- Conflict resolution: deterministic merge by
updated_at/version + field-level wins.
- Tiny UX affordances: offline chip on the header; per-task “unsynced” dot when pending.
UX details
- Status pill:
Online • Synced / Offline • Changes pending (3) (tap → small sheet with a queue list + “Retry now”).
- Per-task subtle indicator when not synced yet (e.g., a small cloud-clock icon).
- Pull-to-refresh on Home triggers a pull sync.
- Errors surface in a non-blocking toast; item remains editable.
Acceptance criteria
Tech notes & approach
Local database
Delta protocol
-
Supabase tasks table: add columns
version BIGINT DEFAULT 0 NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
-
Trigger to bump version on updates:
CREATE OR REPLACE FUNCTION bump_version() RETURNS trigger AS $$
BEGIN NEW.version := COALESCE(OLD.version,0)+1; NEW.updated_at := now(); RETURN NEW; END; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_tasks_version ON public.tasks;
CREATE TRIGGER trg_tasks_version BEFORE UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE bump_version();
-
Push: send pending ops in FIFO; server returns authoritative row (remote_id, updated_at, version). Upsert locally & clear _dirty.
-
Pull: GET rows where updated_at > last_pulled_at. Upsert locally; if local row is _dirty, run conflict resolution:
- If server
updated_at > local base time of edit ⇒ prefer server field, except preserve local _op=delete if server row still exists (tombstone wins).
- Field-level merge: non-overlapping fields keep the freshest
updated_at_field if you choose to track per-field stamps; otherwise start with row-level latest-wins to keep v1 lean.
Optimistic queue
- In memory + persisted (SQLite table
ops_queue with {id, ts, local_id, remote_id, op, payload}).
- On app start and when
NetInfo.isConnected flips true, drain queue with exponential backoff (cap ~30s).
- On auth loss, pause and require re-login.
Realtime coexistence
Background flush
- Use
expo-task-manager + expo-background-fetch to periodically attempt a short push when app is backgrounded (Android/limited iOS; best-effort).
Dev toggles
SYNC_LOG=1 env to console log queue transitions.
- Feature flag
ENABLE_OFFLINE_FIRST=1 to gate rollout.
Testing
- Deterministic unit tests for queue, merge, and error branches using Jest + fake timers.
- E2E happy path: create 3 tasks offline → kill app → reopen → go online → verify remote.
Subtasks
Nice-to-have (later)
- Tombstones for deletes on server to enable true CRDT-ish convergence.
- Per-field vector clocks to get safer merges.
- Selective sync (only current user’s last N months).
- “Force resolve” UI for rare unresolved conflicts.
Why this matters: mobile networks are messy. Going offline-first eliminates spinners, protects user trust, and makes TaskNexus feel premium—even on subway rides and spotty Wi-Fi.
Summary
Make TaskNexus truly mobile-proof by adding an offline-first sync layer: tasks are stored locally, edits apply instantly (optimistic UI), and a background queue syncs deltas to Supabase. Handle conflicts deterministically, show a tiny connectivity badge, and keep everything resilient to flakey networks.
Goals
updated_at/version+ field-level wins.UX details
Online • Synced/Offline • Changes pending (3)(tap → small sheet with a queue list + “Retry now”).Acceptance criteria
updated_atwins by default; partial edit merges for distinct fields (e.g.,completedvstext).Tech notes & approach
Local database
Use
expo-sqlite(orexpo-sqlite/next) for zero-native-mods storage.Table:
tasks_localmirroring Supabase schema + client fields:Lightweight DAO wrapper with typed helpers.
Delta protocol
Supabase tasks table: add columns
versionBIGINT DEFAULT 0 NOT NULL,updated_atTIMESTAMPTZ DEFAULT now() NOT NULLTrigger to bump
versionon updates:Push: send pending ops in FIFO; server returns authoritative row (
remote_id,updated_at,version). Upsert locally & clear_dirty.Pull: GET rows where
updated_at > last_pulled_at. Upsert locally; if local row is_dirty, run conflict resolution:updated_at> local base time of edit ⇒ prefer server field, except preserve local_op=deleteif server row still exists (tombstone wins).updated_at_fieldif you choose to track per-field stamps; otherwise start with row-level latest-wins to keep v1 lean.Optimistic queue
ops_queuewith{id, ts, local_id, remote_id, op, payload}).NetInfo.isConnectedflips true, drain queue with exponential backoff (cap ~30s).Realtime coexistence
Keep existing Supabase realtime subscription. When a realtime event arrives:
_dirty, apply immediately._dirty, stash as a shadow and reevaluate post-push (prevents UI flicker).Background flush
expo-task-manager+expo-background-fetchto periodically attempt a short push when app is backgrounded (Android/limited iOS; best-effort).Dev toggles
SYNC_LOG=1env to console log queue transitions.ENABLE_OFFLINE_FIRST=1to gate rollout.Testing
Subtasks
last_pulled_at) + local upsertNice-to-have (later)
Why this matters: mobile networks are messy. Going offline-first eliminates spinners, protects user trust, and makes TaskNexus feel premium—even on subway rides and spotty Wi-Fi.