Architecture
EstimateIQ runs the whole application — API, UI, and data — from a single Cloudflare Worker,
deployed to esb.guru. The design goal is a synchronous, transactional data layer at the edge
with no round-trips to a remote database on the hot path, and a zero-flash server-rendered UI.
Stack
| Layer | Technology |
|---|---|
| Runtime | Bun (local dev + build); the Cloudflare Workers runtime in production |
| API | Hono — one app (src/app.ts) that mounts every /api route |
| UI | Astro with output: 'server' SSR via @astrojs/cloudflare, embedded in the Worker |
| Data | A Durable Object wrapping synchronous SQLite — the source of truth |
| ORM | A single Drizzle schema (src/db/schema.ts), shared by the Worker, the DO, and D1 |
| Edge services | D1, R2, KV, Queues, Workflows, Vectorize, Workers AI, AI Gateway |
| Infra | Terraform (data plane) + Ansible (data load) — see Infrastructure as code |
The Worker owns everything
The Worker (cloudflare/worker/index.ts) is the front door. On every request it decides where to go:
POST /api/admin/*— handled at the edge, before the Access gate, guarded by a Bearer secret (REVSRC_SECRET_KEY). This is how automation loads data and reindexes without a browser session.- Cloudflare Access — in production, every non-public path must carry a valid Access JWT
(
/healthzis public). /api/ai-brief/:id— reads/writes the persisted next-best-action brief in R2./api/search— Workers AI embeds the query, Vectorize returns matches, KV caches them.- Any other
/api/*or/healthz— forwarded to the data Durable Object, which runs the shared Hono app over synchronous SQLite. - Static assets (
/_astro/*,/brand/*,/docs/*, favicon) — served from Assets. - Everything else — falls through to Astro SSR, rendered server-side from live DO data.
The Worker also exports the Durable Objects and the Workflow so wrangler can bind them, and it
implements two background handlers: queue() (inbound carrier mail) and scheduled() (the hourly
SLA sweep).
Why a Durable Object for data
The primary store is the EstimateIQ Durable Object with an embedded SQLite database
accessed synchronously. This gives EstimateIQ:
- Single-writer consistency — one DO instance (
primary) owns the estimate state machine, so status transitions and ball-in-court updates are serialized and race-free. - Transactional reads and writes with no network latency between the app and its data.
- Automatic co-location of compute and state at the edge.
Because DO SQLite is synchronous, the exact same synchronous repo/actions code runs both in the Bun
dev server and inside the Worker — setDb() just points the shared runtime at whichever database is
active. Tables are created from the shared DDL on first run inside blockConcurrencyWhile, so init
always completes before any request is served.
The DO starts empty — nothing is baked into the image and there is no auto-seed. Data is loaded explicitly through the API (see Data loading).
A second Durable Object, SlaTimerDO, holds one alarm per estimate: when the ball is with us and
a response is due, it arms an alarm that — on fire — enqueues an escalation nudge so a silent estimate
never slips.
Server-side rendering, embedded in the Worker
The UI is Astro with output: 'server', built by @astrojs/cloudflare into a _worker.js
bundle that is imported directly into the front-door Worker. Pages render server-side on every
request with a zero-flash first paint — no client-side loading spinner and no stale build-time
data.
Because the SSR handler runs in the same Worker as the DO, a page reads live data by calling the DO
stub through Astro.locals.runtime.env — no network hop. Any dynamic route (any :id — an estimate,
a carrier, an estimator) is rendered on demand against the DO’s current state. In local
astro dev there is no runtime binding, so the same data helper falls back to an HTTP fetch against
the Hono API.
Cloudflare services in use
| Service | Binding | Purpose |
|---|---|---|
| Durable Objects | ESTIMATEIQ, SLA_TIMERS | The synchronous-SQLite data layer + per-estimate SLA alarms. |
| D1 | DB | Append-only event/audit mirror for cross-instance reporting. |
| R2 | ARTIFACTS | AI briefs (ai/brief/:id.json) + raw .eml correspondence + Xactimate artifacts. |
| KV | CACHE | Semantic-search result cache + last-reindex marker. |
| Vectorize | VECTORIZE | Vector index (estimateiq-notes, 768-dim) for semantic search. |
| Workers AI | AI | bge-base-en-v1.5 embeddings for search indexing + queries. |
| Queues | MAIL_INGEST (+ DLQ) | Inbound carrier-mail ingest, consumed by Worker.queue(). |
| Workflows | REVISION_WF | RevisionWorkflow — the durable revision lifecycle. |
| AI Gateway | — | Authenticated gateway that routes model calls to Claude (see AI). |
| Cron | — | Hourly SLA sweep (0 * * * *) that re-arms SlaTimerDO alarms from the worklist. |
| Assets | ASSETS | The built Astro static bundle, with run_worker_first for /api/* + /healthz. |
Everything ships as one Worker to a single origin, https://esb.guru, on the derekethandavis
Cloudflare account.