Infrastructure & Deployment
Tasuku runs entirely on the Cloudflare edge. A single Worker serves the Astro SSR app, the Hono /api surface, and the static client assets, and it exports the SyncInbox Durable Object. The documentation site you are reading is a separate Cloudflare Pages project. Configuration lives in wrangler.jsonc; all deploys are performed by the maintainer.
Deployment topology
Section titled “Deployment topology”One Worker, two entry points (fetch + scheduled), and the bindings below. Bindings are the only way Worker code reaches external resources (constitution §7.8); secrets (Clerk keys, VAPID private key) are injected separately and never committed.
flowchart TB
user([User browser])
clerk{{"Clerk<br/>identity provider"}}
push{{"Web Push services<br/>(VAPID, HTTPS)"}}
cron(["Cron Trigger<br/>* * * * *"])
subgraph cf["Cloudflare edge"]
direction TB
subgraph appworker["Worker: tasuku — main = src/worker.ts"]
direction TB
ssr["Astro SSR"]
api["Hono API /api/v1"]
sched["scheduled()<br/>reminder scan + completed cleanup"]
do[["SyncInbox<br/>Durable Object<br/>(Hibernatable WS, SQLite)"]]
end
assets[("ASSETS<br/>./dist static + SW")]
d1[("DB → D1<br/>tasuku-db")]
kv[("SESSION → KV<br/>Astro sessions")]
r2[("ATTACHMENTS → R2<br/>tasuku-attachments")]
email["SUPPORT_EMAIL<br/>send_email binding"]
rl["SUPPORT_RATE_LIMITER<br/>Rate Limiting"]
pages["Pages: tasuku-docs<br/>(this site — separate project)"]
end
user -->|"HTTPS"| appworker
user -->|"WSS /api/v1/sync/stream"| do
user -. "sign-in widget" .-> clerk
cron -->|invoke| sched
appworker -->|ASSETS binding| assets
api -->|DB binding| d1
sched -->|DB binding| d1
ssr -->|SESSION binding| kv
api -->|"ATTACHMENTS binding (stream)"| r2
api -->|SUPPORT_EMAIL binding| email
api -->|SUPPORT_RATE_LIMITER binding| rl
api -->|verify session| clerk
api -->|"SYNC_INBOX binding (RPC)"| do
api -. "assignment push" .-> push
sched -. "reminder push" .-> push
Bindings (top-level / production in wrangler.jsonc):
| Binding | Kind | Target | Purpose |
|---|---|---|---|
ASSETS | Static assets | ./dist | Built client + service worker (ADR-009) |
DB | D1 | tasuku-db | Relational source of truth |
SYNC_INBOX | Durable Object | SyncInbox class | Per-user real-time signal inbox (ADR-011) |
SESSION | KV namespace | SESSION | Astro session store (@astrojs/cloudflare) |
SUPPORT_EMAIL | send_email | verified destination | Transactional team email, no API key (ADR-017) |
SUPPORT_RATE_LIMITER | Rate Limiting | per-user | Bounds Support submissions |
ATTACHMENTS | R2 bucket | tasuku-attachments | Private object storage for task file attachments; Worker-streamed, no public access (ADR-019) |
| Cron Trigger | ["* * * * *"] | scheduled() | ~1-min due-reminder scan (ADR-015) + once-daily completed-task cleanup (spec 038) |
The DO binding,
send_email, rate limiter, R2 bucket, Cron Trigger, andmigrationsare non-inheritable by named environments, so they are restated underenv.staging(ADR-011/015/017/019, spec 020). Clerk and Sentry are reached over HTTPS (configured via secrets / a DSN var), not a binding. Web Push is sent directly from the Worker over HTTPS with VAPID (ADR-016) — also not a binding.
How traffic is routed
Section titled “How traffic is routed”The Worker has two entry points: the fetch handler (HTTP/WebSocket, below) and a scheduled handler invoked by the Cron Trigger every minute, which runs the due-reminder scan (fanning out closed-app Web Push) and, behind a once-daily wall-clock gate, the auto-deletion sweep of long-completed Tasks — both independently of any open client (ADR-015/016, spec 038, src/worker.ts). A request entering the fetch handler is dispatched by kind before any business logic runs:
flowchart TD
req["Incoming request"] --> kind{"Path / headers?"}
kind -->|"static asset"| assets["ASSETS → dist/*"]
kind -->|"GET /api/v1/sync/stream<br/>Upgrade: websocket"| up["Clerk auth + Origin/CSWSH check"]
up --> stub["forward to user's SyncInbox stub<br/>101 Switching Protocols"]
kind -->|"/api/**"| mw["Hono middleware chain"]
kind -->|"app route"| ssr["Astro SSR → HTML"]
subgraph chain["Hono middleware (src/api/index.ts)"]
direction TB
sh["secureHeaders (CSP default-src 'none')"] --> oc["originCheck"]
oc --> err["error → RFC 9457 problem+json"]
err --> clerkmw["Clerk auth (single boundary)"]
clerkmw --> route["route handler → repository → D1"]
end
mw --> chain
The WebSocket upgrade is exempted from secureHeaders because its immutable 101 response cannot carry headers; auth and the CSWSH/Origin check still run before the socket is forwarded to the user’s Durable Object stub. See Worker API & Clerk Auth and SyncInbox Durable Object.
Environments
Section titled “Environments”Production and an isolated staging environment are fully separate stacks — separate D1 database, KV namespace, and DO namespace — selected at build time by @astrojs/cloudflare v13 (CLOUDFLARE_ENV), not by wrangler deploy --env (spec 020).
flowchart LR
subgraph prod["Production — worker: tasuku"]
pd[("tasuku-db")]
pdo[["SyncInbox ns"]]
end
subgraph stg["Staging — env.staging"]
sd[("tasuku-db-staging")]
sk[("SESSION KV")]
sdo[["SyncInbox ns"]]
dom["staging.tasuku.grgrt.com<br/>(custom domain)"]
end
prod -. "no shared state" .- stg
Deploy flow
Section titled “Deploy flow”There is no CI deploy job (the project runs under a hard compute cap — CI-cost discipline). The maintainer builds and deploys locally.
flowchart LR
m([Maintainer]) --> b["npm run build<br/>(+ build-sw.mjs)"]
b --> wd["wrangler deploy"]
wd --> appworker["Worker: tasuku"]
m --> mig["npm run db:migrate:*"]
mig --> d1[("D1")]
m --> db["npm run build --prefix docs-site"]
db --> dp["deploy → Cloudflare Pages"]
dp --> docs["tasuku-docs.pages.dev"]
Configuration and operational scripts are catalogued in Operations & Configuration. The app Worker and this docs site are deployed independently (ADR-012).