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).
System context (C4)
Section titled “System context (C4)”At the highest level Tasuku is one system used by a single human actor. Clerk is the only external dependency on the critical path (the authentication boundary); the rest are off-path — Sentry for observability, the Web Push services that deliver closed-app notifications, and an external mailbox that receives transactional support email. Everything else is internal to the Cloudflare deployment (see Infrastructure).
C4Context title Tasuku — System Context Person(user, "User", "Manages tasks across devices, online or offline") System(tasuku, "Tasuku PWA", "Offline-first task manager on the Cloudflare edge") System_Ext(clerk, "Clerk", "Identity provider — the single auth boundary") System_Ext(sentry, "Sentry", "Error / trace / log / metric monitoring (PII-scrubbed, ADR-018)") System_Ext(push, "Web Push services", "Browser push endpoints — closed-app notifications (ADR-016)") System_Ext(mail, "Support mailbox", "External address receiving transactional team email (ADR-017)") Rel(user, tasuku, "Manages lists & tasks", "HTTPS / WebSocket") Rel(user, clerk, "Signs in", "HTTPS") Rel(tasuku, clerk, "Verifies session", "HTTPS") Rel(tasuku, sentry, "Reports errors & telemetry", "HTTPS") Rel(tasuku, push, "Sends notifications", "HTTPS / VAPID") Rel(tasuku, mail, "Sends support email", "Email Routing") UpdateRelStyle(user, tasuku, $offsetY="-30") UpdateRelStyle(tasuku, sentry, $offsetX="-40")
Container view (C4)
Section titled “Container view (C4)”Zooming one level in, the containers — the separately runnable/deployable units — split across the user’s device (the browser PWA and the tasuku command-line client) and the Cloudflare edge. They are joined by HTTPS for the request/write path and a WebSocket for real-time signals. Each container is independently replaceable; what stays stable between them is the contract — the versioned /v1 API and the tiny SyncSignal. Both first-party clients (web PWA and CLI) go through the same /v1 API and the same Clerk auth boundary (S14, ADR-004/005/014).
C4Container
title Tasuku — Container View
Person(user, "User", "Manages tasks across devices, online or offline")
Container_Boundary(device, "User's device — browser") {
Container(spa, "PWA Client", "Astro + React islands, TypeScript", "Static pages hydrate small interactive islands; reads, search and filtering run fully offline")
Container(sw, "Service Worker", "Workbox", "Precaches the app shell and assets for offline launch")
ContainerDb(dexie, "Client Store", "Dexie / IndexedDB", "Read cache plus the mutation outbox")
}
Container_Boundary(terminal, "User's device — terminal") {
Container(cli, "tasuku CLI", "Node, TypeScript (npm)", "Scriptable first-class client; OAuth PKCE; same /v1 API; no local data")
}
Container_Boundary(edge, "Cloudflare edge") {
Container(app, "Edge Application", "Cloudflare Worker — Astro SSR + Hono API /v1", "Serves the app shell and the versioned API; the single Clerk auth boundary; src/worker.ts")
Container(do, "SyncInbox", "Durable Object — hibernatable WebSockets", "Per-user inbox that pushes change signals")
ContainerDb(d1, "Database", "Cloudflare D1 (SQLite) + Drizzle", "Relational source of truth")
ContainerDb(kv, "Session Store", "Cloudflare KV", "Astro server sessions")
ContainerDb(r2, "Attachment Store", "Cloudflare R2", "Private bucket for task file bytes (ADR-019)")
Container(cron, "Cron Trigger", "scheduled()", "Due-reminder scan + daily completed-task cleanup")
}
System_Ext(clerk, "Clerk", "Identity provider — the single auth boundary")
System_Ext(sentry, "Sentry", "Monitoring (ADR-018)")
System_Ext(push, "Web Push services", "Closed-app notifications (ADR-016)")
System_Ext(mail, "Support mailbox", "Transactional email (ADR-017)")
Rel(user, spa, "Uses", "HTTPS")
Rel(user, cli, "Runs commands", "terminal")
Rel(user, clerk, "Signs in", "HTTPS")
Rel(cli, app, "Calls the API with a bearer token", "HTTPS /api/v1")
Rel(cli, clerk, "OAuth Auth-Code + PKCE", "HTTPS")
Rel(sw, spa, "Precaches shell & assets", "Cache Storage")
Rel(spa, dexie, "Caches reads / queues writes", "IndexedDB")
Rel(spa, app, "Loads shell, replays outbox, delta reads", "HTTPS /api/v1")
Rel(spa, do, "Receives change signals", "WSS")
Rel(app, clerk, "Verifies session", "HTTPS")
Rel(app, d1, "Reads & writes", "Drizzle")
Rel(app, kv, "Stores sessions", "KV")
Rel(app, r2, "Streams attachment bytes", "R2 binding")
Rel(app, do, "Fans out signals off the request path", "RPC / waitUntil")
Rel(app, mail, "Sends support email", "send_email binding")
Rel(cron, d1, "Scans & sweeps", "Drizzle")
Rel(cron, push, "Sends reminders", "HTTPS / VAPID")
Rel(app, push, "Sends assignment pushes", "HTTPS / VAPID")
Rel(spa, sentry, "Reports errors & telemetry", "HTTPS")
Rel(app, sentry, "Reports errors & telemetry", "HTTPS")
UpdateRelStyle(spa, do, $offsetY="-40")
UpdateRelStyle(app, do, $offsetX="-40", $offsetY="-10")
- PWA Client — the Astro pages and React islands that render the UI; all reads, search, and filtering resolve against the local store, so the app is usable with no network.
- Service Worker — a Workbox precache that lets the app shell launch offline (ADR-009).
- Client Store (Dexie) — the only client store: a read cache of the user’s data plus the write outbox.
- tasuku CLI — the user-facing command-line client (a separate npm package); a thin, scriptable consumer of the same
/v1API with no business logic and no local data store. It authenticates with OAuth 2.0 Authorization Code + PKCE (Clerk as IdP) and presents a bearer token verified at the same boundary as a web session (S14; ADR-004/005/014). See the CLI guide. - Edge Application — Astro SSR and the Hono
/v1API in a single Worker (src/worker.ts); the only container that talks to D1 and the only place Clerk sessions are verified. - SyncInbox — the per-user Durable Object that holds hibernatable WebSockets and pushes change signals (ADR-011).
- Database (D1) — the relational source of truth, reached only through Drizzle repositories.
- Session Store (KV) — Cloudflare KV backing Astro’s server-side sessions.
- Attachment Store (R2) — a private Cloudflare R2 bucket holding task file bytes; reached only through the Worker (no public access), keyed by an opaque
storageKey(ADR-019). - Cron Trigger — the Worker’s
scheduled()entry point: a ~1-minute due-reminder scan plus the once-daily completed-task cleanup, both writing D1 through the same repositories (ADR-015, spec 038).
Clerk is the only critical-path external dependency. The off-path externals — Sentry (monitoring), the Web Push services (notifications), and the support mailbox (email) — are reached over HTTPS / Email Routing, never on the read/write critical path. For where each container is actually deployed and how the Worker is bound to D1, KV, R2, and the Durable Object, see Infrastructure.
Components
Section titled “Components”- 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/clientand 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 bycreateApp()insrc/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). SyncInboxDurable Object — the project’s first 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).
- Scheduled jobs (Cron Trigger) — the Worker’s
scheduled()handler runs a ~1-minute due-reminder scan (ADR-015,src/worker.ts→src/lib/server/notify.ts) and, behind a once-daily wall-clock gate, the auto-deletion sweep of Tasks completed over 30 days ago (spec 038,src/lib/server/completed-cleanup.ts) — both independent of any open client. Stateless per the isolate model; reads/writes D1 through repositories; the reminder scan no-ops when Web Push is unconfigured. - Web Push delivery — closed-app OS notifications over the Web Push protocol with VAPID + aes128gcm on WebCrypto (ADR-016,
src/lib/server/webpush.ts); fan-out reuses the sameaccessible()set as sync. Reminders fire from the cron; assignments fire on the write path viawaitUntil. - Outbound email — transactional team email via Cloudflare Email Routing’s
send_emailbinding, pinned to a verified destination (ADR-017,src/api/lib/support-send.ts). Online-only, free-tier, no API-key secret. - Observability (Sentry) — error, performance-trace, log, and metric monitoring over the official SDKs, behind a vendor-neutral facade (
src/lib/obs/report.ts) with two adapters (Worker + browser); PII-scrubbed (ADR-018). See Observability.
One level deeper still — a component view that breaks the edge application into its internal layers (SSR, the Hono API, and the Drizzle repositories). Solid arrows are the request/write path, the dashed arrow is the real-time signal that the client turns back into a delta read:
flowchart LR
user([User])
subgraph browser["Browser (PWA)"]
UI["Astro pages + React islands<br/>src/islands, src/pages"]
SW["Service worker<br/>Workbox precache"]
DX[("Dexie / IndexedDB<br/>read cache + outbox")]
end
subgraph worker["Cloudflare Worker — src/worker.ts"]
SSR["Astro SSR"]
API["Hono API /api/v1<br/>createApp()"]
SCHED["scheduled()<br/>reminder scan + completed cleanup"]
REPO["Drizzle repositories<br/>src/db/repositories"]
DO[["SyncInbox<br/>Durable Object"]]
end
D1[("D1 — source of truth")]
R2[("R2 — attachment bytes")]
clerk{{"Clerk"}}
cron(["Cron Trigger"])
push{{"Web Push (VAPID)"}}
user --> UI
SW -. precache shell .-> UI
UI -->|optimistic write| DX
UI -->|read / search / filter| DX
DX -->|replay outbox| API
UI -->|"delta read ?since"| API
UI -. initial load .-> SSR
API -->|authorize| clerk
API --> REPO --> D1
API -->|"attachment bytes (stream)"| R2
API -->|"fan-out signal (waitUntil)"| DO
DO -. "SyncSignal (WS)" .-> UI
cron --> SCHED --> REPO
SCHED -. "reminder push" .-> push
API -. "assignment push" .-> push
Request and sync flow
Section titled “Request and sync flow”A user action follows the offline-first path (Product Brief §5.2, “Sharing and sync rules”):
- Optimistic local write — the island applies the change to the Dexie cache and enqueues it in the outbox; the UI updates immediately.
- Replay to the server — when online, the outbox replays the mutation to the Worker API; an idempotency key lets the server deduplicate replays (
idempotencyKeystable,src/db/schema.ts). - Authoritative write — the Worker validates (Zod), authorizes (Clerk session + the ownership/membership rule), and writes to D1 through a repository function.
- 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’sSyncInboxto broadcast aSyncSignalnaming the changed surface (ADR-011;affectedUsers()/notifyList()insrc/api/lib/notify.ts). - Client reacts — a connected client runs its existing
?sincedelta 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).
The same five steps, end to end across the origin client, the server, and a second connected client:
sequenceDiagram actor User participant Island as React island participant Dexie as Dexie (cache + outbox) participant API as Worker API (Hono) participant D1 as D1 participant DO as SyncInbox DO participant Peer as Other device User->>Island: edit task Island->>Dexie: optimistic write + enqueue outbox Island-->>User: UI updates immediately Note over Island,API: when online Dexie->>API: replay mutation (+ idempotency key) API->>API: validate (Zod) + authorize (Clerk) API->>D1: write via repository API-->>Dexie: ack (deduped on replay) API-)DO: notify affected users (waitUntil) DO-)Peer: SyncSignal "surface changed" Peer->>API: delta read ?since API-->>Peer: changed rows Peer->>Peer: reconcile into Dexie cache
For the connection lifecycle, coalescing, and the polling fallback in depth, see Sync & Real-Time Architecture.
Transport evolution
Section titled “Transport evolution”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.