Skip to content

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.

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):

BindingKindTargetPurpose
ASSETSStatic assets./distBuilt client + service worker (ADR-009)
DBD1tasuku-dbRelational source of truth
SYNC_INBOXDurable ObjectSyncInbox classPer-user real-time signal inbox (ADR-011)
SESSIONKV namespaceSESSIONAstro session store (@astrojs/cloudflare)
SUPPORT_EMAILsend_emailverified destinationTransactional team email, no API key (ADR-017)
SUPPORT_RATE_LIMITERRate Limitingper-userBounds Support submissions
ATTACHMENTSR2 buckettasuku-attachmentsPrivate 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, and migrations are non-inheritable by named environments, so they are restated under env.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.

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.

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

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).