Skip to content

Offline Outbox & Optimistic Writes

src/lib/offline/db.ts defines the Dexie (IndexedDB) database — the sole client-side store (constitution §7.14.1). It holds a read cache of the user’s data plus the persistent mutation outbox. It never stores auth tokens, credentials, or secrets (§7.14.5).

The diagram below shows the write side: optimistic API → Dexie cache + outbox → replay to D1.

flowchart LR
  UI["Islands / UI"] -->|"createList(), renameList(),<br/>deleteList(), moveList(), …"| API["sync.ts<br/>optimistic write API"]
  API -->|"1. apply optimistically"| DX[("Dexie read cache")]
  API -->|"2. enqueue OutboxEntry"| OB[("Dexie outbox table")]
  OB -->|"buildRequest(entry)<br/>sync-core.ts"| REQ["HTTP request<br/>+ Idempotency-Key"]
  REQ -->|"3. replay when online"| HONO{{"Hono API"}}
  HONO --> D1[("D1 — source of truth")]
  D1 -.->|"idempotencyKeys table<br/>dedupes replays"| HONO
  • The outbox table (OutboxEntry) queues mutations made locally, indexed by createdAt and entityId (for dependency lookups). The Dexie schema is versioned (v1…v8+) as read models were added per feature.
  • Each entry carries a stable Idempotency-Key so replays are deduplicated server-side (the idempotencyKeys D1 table) — replaying the outbox is safe and idempotent (constitution §7.14.3).
  • buildRequest(entry) (in sync-core.ts) turns an outbox entry into the HTTP request the replay sends.

src/lib/offline/sync.ts exposes the user-facing mutations (createList, renameList, deleteList, moveList, duplicateList, createGroup, deleteGroup, moveGroup, …). Each:

  1. applies the change to the Dexie cache and shows it immediately (optimistic);
  2. enqueues an outbox entry;
  3. replays to the server when online, where it is reconciled into D1 (the source of truth).

When offline, writes queue and replay on reconnect; there is no on-device primary database and no sync engine — the client is a cache plus outbox (Product Brief §1.1).

A single optimistic mutation flows from instant local apply through deferred replay:

sequenceDiagram
  participant U as "User"
  participant S as "sync.ts"
  participant DX as "Dexie (cache + outbox)"
  participant H as "Hono API"
  participant D1 as "D1"
  U->>S: "renameList(id, title)"
  S->>DX: "apply to cache (optimistic)"
  S->>DX: "enqueue OutboxEntry (Idempotency-Key)"
  Note over U,DX: "shown immediately — works offline"
  S->>S: "buildRequest(entry)"
  S->>H: "replay when online"
  H->>D1: "apply + record idempotencyKey"
  D1-->>H: "ok (replays deduped)"
  H-->>S: "confirmed → mark synced"

Related: Sync-Core Reconcilers (the read/fold side), DB Schema & Notify-Affected (the server tables + idempotency).