An opinionated, event-driven integration platform on Cloudflare Workers — batteries included, agent-native.
Most Workers starters give you a router and a hello world. Real integration work — webhooks from vendors, payments, ERPs, carriers — needs more: durable events, idempotency, dead-letter queues, reconciliation, structured observability, a read-only surface an agent can safely query, and a dashboard. rafoworks is that skeleton, distilled from a production platform, with the vendor-specific parts stripped out and one clean, runnable example of each primitive left in.
- Why an integration platform, not a router. Integrations fail in ways a request/response app doesn't: a webhook arrives twice, a downstream is down for an hour, an event gets dropped, two events race. A router has no answer for any of these. rafoworks bakes in the answers — dedup, durable retries, a reconciliation net — so you don't rediscover them in an incident.
- Why event-driven. Turning every inbound webhook into a durable, idempotent event (webhook → queue → workflow → D1) decouples receiving from processing. The queue becomes an outage buffer; the workflow gives you durable, resumable execution; the domain-events arm lets business consumers subscribe without touching ingestion.
- Why opinionated. Every convention here prevents a class of failure that a typechecker can't see — a race, silent drift, a broken contract. They're written down as resilience invariants, each with the trap and the real code that avoids it. Opinionated defaults are the point: you inherit the scars, not the incidents.
- Why Cloudflare Workers. D1, KV, Queues, Workflows, and R2 are one platform, at the edge, with a generous free tier and no servers to run. The whole pipeline in this repo deploys as three small workers.
packages/
shared/ domain — entities, ports, pure rules, event envelope, error taxonomy (no I/O)
api/ the Worker — adapters, services, workflows, routes, the composition root
mcp/ a read-only MCP server (service binding → the api's ToolsEntrypoint)
web/ a React + Vite dashboard, served as a Worker via Static Assets
scripts/ the enforcement — deploy-gate, docs-lint, changelog-guard/collate
docs/ durable × dated docs with a machine-readable frontmatter contract
.claude/ hooks (the gate) + the destructive-write deny list
CLAUDE.md the operating manual for humans and agentsThe one worked example runs the whole pipeline end to end:
POST /webhooks/example ─▶ ingress (auth, dedup, archive to R2, record) ─▶ [queue]
─▶ dispatch ─▶ OrderSyncWorkflow (durable, idempotent) ─▶ upsert order in D1
─▶ emit domain event ─▶ [domain-events queue] ─▶ business consumers
▲
hourly reconcile cron re-drives anything stuck ┘A fat webhook (carrying the order) materializes the order directly; a thin one (just an id) makes the workflow fetch it from the source adapter. Swap ExampleSourceAdapter for a real vendor and nothing else changes — that's the point of the ports.
- Node ≥ 20 and pnpm 10 (
corepack enablegives you pnpm). - A Cloudflare account (free tier is enough) — only needed to deploy, not to run locally.
No Cloudflare account needed; everything runs in Miniflare.
pnpm install
# secrets for local `wrangler dev`
cp packages/api/.dev.vars.example packages/api/.dev.vars
# create the local D1 schema (the migration is committed under packages/api/drizzle)
pnpm --filter @rafoworks/api db:migrate:local
# boot the api worker on http://localhost:8787 (queues + workflows run locally too)
pnpm devNow drive the whole pipeline with one webhook (the token matches .dev.vars):
curl -sX POST http://localhost:8787/webhooks/example \
-H "x-webhook-token: dev-webhook-token" \
-H "content-type: application/json" \
-d '{"event":"order.paid","order_id":"o-1","status":"paid","customer_name":"Ada Lovelace","total_cents":4990}'
# → {"ok":true,"status":"queued"}
# the queue consumer + OrderSyncWorkflow run; a moment later the order is materialized:
curl -s http://localhost:8787/orders/example:o-1
# → {"id":"example:o-1","source":"example","sourceOrderId":"o-1","status":"paid",
# "customerName":"Ada Lovelace","totalCents":4990, ...}
# send the same payload again — deduped, no duplicate (idempotent by construction):
curl -sX POST http://localhost:8787/webhooks/example -H "x-webhook-token: dev-webhook-token" \
-H "content-type: application/json" -d '{"event":"order.paid","order_id":"o-1","status":"paid"}'
# → {"ok":true,"status":"duplicate"}See it in the dashboard (a second terminal):
VITE_API_BASE=http://localhost:8787 pnpm --filter @rafoworks/web dev
# open the printed URL — "Recent orders" lists what you just creatednpx wrangler login # or set CLOUDFLARE_API_TOKEN (see .envrc.example)
# 1. create the resources (names are yours to choose)
npx wrangler d1 create rafoworks-db
npx wrangler kv namespace create CACHE
npx wrangler r2 bucket create rafoworks-raw-data
npx wrangler queues create webhook-events
npx wrangler queues create webhook-events-dlq
npx wrangler queues create domain-events
npx wrangler queues create domain-events-dlq
# 2. paste the printed database_id / kv id into packages/api/wrangler.toml
# 3. set the api secrets
cd packages/api
npx wrangler secret put EXAMPLE_API_TOKEN
npx wrangler secret put EXAMPLE_WEBHOOK_TOKEN
cd ../..
# 4. apply migrations to the remote D1
pnpm --filter @rafoworks/api db:migrate
# 5. deploy (the gate runs first and blocks on any red check)
pnpm deploy:api
pnpm deploy:mcp
pnpm build && pnpm deploy:webEvery change must pass before it ships. Run it any time:
pnpm gate # typecheck + lint + build + test + docs-lint + changelog-guardA PreToolUse hook runs the same checks before any wrangler deploy and denies the deploy on failure — because wrangler deploy runs none of them on its own. The example ships with 22 tests, including seam tests against a real D1 (vitest-pool-workers).
Mutation testing is the real measure of test strength, and one of this repo's forces. Coverage says a line ran; Stryker mutates the code (flips > to >=, deletes a branch, blanks a string) and re-runs the tests — a mutant that survives is a hole your suite doesn't catch, even at 100% coverage. pnpm mutation:shared scores 100% on the domain rules; pnpm mutation:api guards the core services. It runs as its own CI job, kept out of the gate so it never slows a deploy. The rule: kill the mutant, don't lower the bar.
Deep dives on how each piece works — every one grounded in the real code, not hand-waving. Index at docs/README.md.
Understand the platform:
- Architecture — hexagonal + event-driven, the resilience invariants, platform limits.
- The event-driven pipeline — one webhook traced end to end: ingress → queue → dispatch → workflow → D1 → domain events.
- Durable workflows — Cloudflare Workflows:
step.do, resume-after-crash, idempotent steps, deterministic instance ids. - Observability — structured logs → wide events → CF Logs/Traces over OTEL to Axiom, with real JSON and query examples.
- Resilience invariants — the six invariants, each with the trap and the code that avoids it.
- Testing — unit vs seam tests (against a real D1) and mutation testing.
Do things:
- Add an adapter — a runbook to plug in a real source end to end.
- The MCP server — the read-only agent surface + putting OAuth in front of it.
And the operating manual, CLAUDE.md — the conventions, invariants, and platform limits an agent (or human) works under here.
Add an adapter (the common case):
- Define or reuse a port in
packages/shared/src/ports/. - Implement it as a class with constructor injection in
packages/api/src/adapters/, validating the vendor response at the boundary with zod. - Wire it in
packages/api/src/lib/container.ts(the composition root) — the only place concretes meet the environment. - Add a unit test for the mapping and, if it crosses a critical seam, an integration test (see
packages/api/src/services/webhook-ingress.test.ts).
Add a workflow: create a WorkflowEntrypoint in packages/api/src/workflows/, export it from index.ts, add a [[workflows]] block to wrangler.toml, and route to it in services/event-router.ts.
This repo assumes an AI is writing code in it, and instruments accordingly:
- The deploy gate blocks a deploy on any red check (typecheck/lint/build/test/docs/changelog).
- changelog-guard denies a deploy of changed code with no changelog entry — "shipped ⟹ recorded", enforced from git history.
- docs-lint keeps the docs contract (frontmatter, resolving links, no future-intent in durable docs) valid at commit time.
- A deny list blocks destructive production SQL from the agent, even with verbal approval.
For the workflow itself, pair it with rafoflow — an opinionated Shape Up agent fleet for Claude Code:
/plugin marketplace add rafae2k/rafoflow
/plugin install shapeup@rafoflowThen /cycle "your goal" orchestrates research → shape → bet → scope → build → review → ship, leaving an artifact trail in docs/cycles/. rafoflow ships the method; this repo ships the enforcement that makes it non-optional. Read CLAUDE.md for the full operating manual, and the documentation above for how every piece works.
See CONTRIBUTING.md. In short: keep it generic and minimal, pnpm gate green, pnpm mutation green for changed logic.
Built by Rafo. MIT.