Skip to content

System Architecture

Tasuku is an offline-first Progressive Web App on the Cloudflare edge. The server-side D1 database is the single source of truth; the client is a cache plus an outbox, never a second source of truth (Product Brief §1.1, constitution §7.14).

  • Astro app + React islands — static Astro pages hydrate small interactive React islands (src/islands, src/pages, src/layouts). Interactivity is opt-in per island to keep the JS budget small (constitution §8.1–§8.2).
  • Service worker (Workbox) — built post-build over dist/client and paired with a prerendered app-shell route; precaches the shell and assets for offline launch (ADR-009, constitution §7.14.2).
  • Client store (Dexie/IndexedDB) — the sole client store: a read cache of the user’s data plus the mutation outbox. Reads, search, and filtering work offline; writes are optimistic and queued (Product Brief §1.1).
  • Worker API (Hono) — a Cloudflare Worker serving the HTTP API under /api, built by createApp() in src/api/index.ts. Contract-first via @hono/zod-openapi, versioned under /v1 (ADR-001, ADR-003).
  • D1 + Drizzle — the relational source of truth; typed access through repository functions in src/db/repositories (constitution §7.10).
  • SyncInbox Durable Object — the project’s first and only Durable Object; a per-user inbox holding hibernatable WebSockets that pushes tiny change signals (ADR-011, src/durable-objects/sync-inbox.ts).
  • Clerk — the single authentication boundary; the Worker derives the user identity from the verified session (constitution §7.12).

A user action follows the offline-first path (Product Brief §5.2, “Sharing and sync rules”):

  1. Optimistic local write — the island applies the change to the Dexie cache and enqueues it in the outbox; the UI updates immediately.
  2. Replay to the server — when online, the outbox replays the mutation to the Worker API; an idempotency key lets the server deduplicate replays (idempotencyKeys table, src/db/schema.ts).
  3. Authoritative write — the Worker validates (Zod), authorizes (Clerk session + the ownership/membership rule), and writes to D1 through a repository function.
  4. Fan-out signal — after a successful write, the Worker computes the affected users (owner ∪ active members) and, off the request path (waitUntil), RPC-calls each user’s SyncInbox to broadcast a SyncSignal naming the changed surface (ADR-011; affectedUsers()/notifyList() in src/api/lib/notify.ts).
  5. Client reacts — a connected client runs its existing ?since delta read for that surface and reconciles into Dexie. The WebSocket replaces the polling timer, not the data path; while disconnected, a slow polling fallback + a catch-up read on reconnect preserve convergence (ADR-006 → ADR-011).

Real-time propagation began as periodic polling for the MVP (ADR-006) and was upgraded to signal-only WebSocket push over the SyncInbox Durable Object (ADR-011, constitution §7.14.6). Polling remains the documented fallback. Offline-first is unchanged throughout: the server stays the source of truth and the client degrades to cached reads + outbox replay.

Concrete code paths are covered in the module guides: Worker API & Clerk Auth, Sync-Core Reconcilers, SyncInbox Durable Object, LiveConnection WS Client, and Offline Outbox & Checklists.