flowchart LR
Client(["Client\n(web / PWA)"])
subgraph pub["Public"]
API["API Worker"]
end
subgraph priv["Private Workers"]
Auth["Auth Service"]
User["User Service"]
Notif["Notification Service\n(3 workers + DO)"]
Contact["Contact Service"]
end
subgraph dos["Durable Objects"]
LabourDO[("Labour DO\none per labour\nevent store Β· read models Β· ledger")]
end
D1[("D1\nshared read models")]
Clerk[["Clerk"]]
Resend[["Resend"]]
Twilio[["Twilio"]]
Slack[["Slack"]]
Client -- "HTTPS" --> API
Client == "WebSocket" ==> LabourDO
API -- "service binding" --> Auth
API -- "service binding" --> User
API -- "command or\nper-labour query" --> LabourDO
API -- "cross-aggregate\nquery" --> D1
LabourDO -. "async projector" .-> D1
LabourDO -- "service binding" --> Notif
User -- "fetches data" --> Clerk
Auth -- "verify JWT" --> Clerk
Notif -- "email" --> Resend
Notif -- "SMS / WhatsApp" --> Twilio
Contact -. "writes" .-> D1
Contact -- "alert" --> Slack
The backend is a full rewrite of the original Python/GCP stack onto Cloudflare Workers and Durable Objects in Rust. Each labour is its own Durable Object with its own SQLite event store, projections, and WebSocket subscribers.
Commands run synchronously on a single thread inside the DO, so appends never race, and an alarm fires immediately after the response to run sync projectors (DO-local SQLite), broadcast events to connected WebSockets, run async projectors (to D1, for cross-labour queries), and dispatch effects via a process manager.
Side effects (notifications, issuing follow-up commands, generating subscription tokens) go through a policy/effect ledger with per-effect idempotency keys, so alarm retries canβt double-send.
See Event sourcing on Cloudflare Workers and Durable Objects for a full walkthrough of the architecture, and the earlier Fern Labour (legacy) project for the original Python/GCP backend it replaced.