A multi-tenant SaaS starter kit for Cloudflare Workers — copy it, rename it, ship an internal tool or a B2B product.
Rocketflare is one repository that already solves the platform work every internal or B2B product
needs before its first real feature: tenancy, sign-in, roles and permissions, background jobs,
realtime, file storage, analytics dashboards, an AI layer (chat, agents, retrieval) and a CLI —
running as a single Cloudflare Worker over Postgres, with a React UI and a shared contract package
that keeps the API, the UI and the CLI in agreement. The first local run needs zero external
credentials, and the codebase is written to be driven by a coding agent: CLAUDE.md,
.claude/rules/, per-directory guides and a "how it works" reference mean the next feature is a
contract → schema → route → page (→ command) loop, not a platform project.
See Rocketpod for a version of Rocketflare that runs without Cloudflare (Node in containers, Postgres for everything).
The kit is a starting point, not a dependency: take a copy, cut it loose from this repository's
history, and make it your own — you will rename, delete and rewrite freely, and you never merge from
here. Cut loose is not cut off: your copy keeps a .rocketflare.json saying where it came from, and
/rf-upgrade ports later kit releases into it without recreating anything you deleted.
Ask your coding agent. Open the copy in Claude Code and type /rf-setup — it checks the
toolchain, starts Postgres, migrates, seeds the demo workspace and leaves you signed in, showing one
verification line per step. Then /rf-adapt <slug> "Your App" renames the kit to your app, and
/rf-provision deploys it (you type that one yourself: it creates paid resources and prompts for
vendor tokens). /rf-plugin installs a capability as a plugin — a git repository copied into your
app, with the plan shown before anything is written. Later, when the kit has moved on,
/rf-upgrade.
One command. Read scripts/install.sh first — it is short — then:
curl -fsSL https://rocketflare.dev/install.sh | bash -s -- myapp # clone → detach → bash scripts/bootstrap.shBy hand, if you would rather see each step (macOS or Linux; Windows through WSL2):
git clone https://github.com/rocketflare-dev/rocketflare.git myapp && cd myapp
rm -rf .git && git init && git add -A && git commit -m "Start from Rocketflare" # your history starts here
git remote add origin git@github.com:<you>/myapp.git # your own repo, when ready
bash scripts/bootstrap.sh # checks Node 24 / pnpm 10 / Docker, generates the local secret, starts
# Postgres, migrates, seeds demo data, starts the app and opens the
# browser signed in as the demo owner. Re-runnable.
# --offline skip the Cloudflare login by disabling Workers AI ([ai] off in both tomls)
# --no-demo seed the bare tenant and users only (plain `pnpm seed`)
# --db-url <url> no Docker: use an existing Postgres (a Neon branch)
# --driver neon|postgres the LOCAL database driver (default postgres)SETUP.md Part 1 is the same thing written out, one verification line per step.
Nothing external is required: no RESEND_API_KEY → magic-link URLs are logged by wrangler dev; no AI
key → chat, agents and embeddings run on Workers AI through the [ai] binding (billed to your
Cloudflare account, 10k free neurons/day); no Cloudflare login, or zero-spend wanted → --offline.
Then pnpm gate — lint, typecheck, test and build, the same command CI runs; pnpm test starts a
throwaway Postgres on :5433 and the Neon proxy itself, runs the suite, then proves the driver seam
under the Neon driver. Before
building your app, rename it: /rf-adapt or docs/ADAPTING.md. Later,
/rf-upgrade (or pnpm kit:upgrade) reads .rocketflare.json and the release notes in
docs/upgrades/, translates the kit's diff into your names, and skips every part
you removed. CHANGELOG.md is what you would be catching up on.
| Layer | Choice |
|---|---|
| Runtime | Cloudflare Workers (nodejs_compat): one Worker exports fetch + queue + scheduled, a Durable Object and a Workflow |
| API | Hono 4, zod contracts, CASL abilities, JSON error envelope everywhere |
| Database | Postgres 17 + pgvector — Docker locally; deployed, Neon over its serverless driver (a fresh copy's default) or any Postgres through Hyperdrive and postgres.js (DATABASE_DRIVER, D35); Drizzle, one client per request |
| UI | React 18 + Vite, DaisyUI 5 on Tailwind v4, React Router 6, TanStack Query 5; served as Workers Static Assets |
| CLI | commander + chalk; browser login → tenant API key; --json on every list command |
| Async / realtime | Queues, Workflows, a per-tenant Durable Object over WebSockets, cron triggers, R2 |
| AI | Anthropic / OpenAI-compatible / Workers AI chat over SSE, agents on Workflows, Workers AI embeddings → pgvector, OTLP tracing (Langfuse, Phoenix, any backend), evals (vitest-evals) with thumbs feedback |
| Analytics | drizzle-cube semantic layer (/cubejs-api, /mcp), fact tables on a cron, TypeScript dashboard templates |
| Quality | Biome 2, strict TypeScript, vitest against real Postgres, gitleaks, one CI gate |
- Multi- or single-tenant from one schema —
TENANCY_MODE=multi|single. Every domain row carries atenant_id; single mode auto-joins users to the one organisation and hides the org switcher. Flip later with no migration. - Sign-up policy as configuration —
SIGNUP_MODE=open|invite_only|approval: personal tenants, invite-only, or an access-request queue that global admins approve into a new or existing tenant, with an optional email-domain allow-list. - Roles and abilities —
owner | admin | memberper tenant plus a platformsupportrole and global admins; CASL abilities are computed server-side and shipped to the UI (<Can>,RequireAbility) so pages and nav use the same guard as the route. - Invitations and members — create, bulk-invite, resend, revoke, accept; change roles, remove members, transfer ownership; activity log of everything.
- Admin area —
/adminfor global admins: tenants, users, access requests; "enter" a customer tenant assupportwith a real membership row, so the single "must be a member" invariant never bends. - Isolation by predicate, RLS in reserve — every query filters by the session's tenant; every tenant table also ships a row-level-security policy, inert until
TENANT_SCOPE_MODE=enforce(docs/RLS.md), with a catalog test that fails CI if a table is missed.
- Magic link (random single-use token, hashed at rest) — works with no email provider: the URL is logged locally.
- OAuth registry — Google and Microsoft via arctic; adding a provider is one definition file. Account linking by verified email, tokens AES-GCM encrypted, PKCE state in one cookie.
- Any OIDC issuer (Okta, Entra ID, Keycloak, Auth0…) — off until
OIDC_ISSUERis set: discovery, PKCE + nonce,id_tokenverified against the issuer's JWKS.AUTH_OIDC_ONLYmakes it the only sign-in; logout follows the issuer'send_session_endpoint. - Sessions as rows —
__Host-sessioncookie, 7-day sliding TTL, one LATERAL query per request. - Tenant API keys — hashed, scoped, expirable, revocable from Settings; the Bearer path shares the auth middleware and abilities with the UI.
- Hardening built in — CSRF by origin allow-list, KV sliding-window rate limits on login routes, security headers, body limits, dev-login that 404s in production,
gitleaksin CI.
- Jobs queue — one
JOBS_QUEUEwith typed envelopes (email.send,activity.record,document.index, …); invalid messages are acked (never loop), handler errors retry with capped backoff; a missing binding throws rather than running work inline. - Workflows — durable multi-step runs (
claim → execute → finish) where the database row is the claim, so retries re-claim and settled rows are never rewritten; no in-memory concurrency anywhere. - Cron — a dispatcher keyed on the cron expression: nightly pruning, hourly fact-table refresh.
- Realtime hub — one stateless Durable Object per tenant on the hibernation API; the server "nudges" (
member.changed,invitation.changed,entity.changed { entity, id }…) and the UI re-queries. The database is the truth; the WebSocket is a nudge. Reconnecting client with backoff, header status dot, outage banner.
- Email through Resend over plain
fetchwith shared templates (magic link, invitation, accepted, access decision); absent an API key, messages are logged, never failed. Invitation mail is queued; the magic link stays inline because someone is waiting. - File storage on R2 behind a
StorageServiceseam: tenant-prefixed keys, bytes streamed through the Worker, an indexedfilestable, per-scope MIME and size limits, avatars wired end-to-end (upload UI →/api/files/:idwith ETag/304).
- Tiered provider resolution with a zero-key floor — per-agent model assignment → the tenant's own provider (keys encrypted at rest, tested from Settings → AI) → a platform key → Workers AI through the binding (
@cf/zai-org/glm-4.7-flashby default, no key, billed to the Cloudflare account) → a clean 503. Providers: Anthropic, Anthropic-compatible (Fireworks, Moonshot presets), OpenAI, OpenAI-compatible (any local server such as Ollama), Workers AI for chat and embeddings. - Streamed chat — conversations and messages persisted per user, SSE frames with a shared event contract, auto-titles, prompt caching breakpoints, extended thinking off unless a tenant turns it on.
- Prompt registry — prompts are code with
{{variables}}; tenants override them in Settings → Prompts and revert with one click. - Agents on Workflows —
POST /api/agents/runsenqueues and answers 202; runs are exclusive per tenant and agent via a partial unique index, emit a durable event timeline, cancel cooperatively, and reconcile against the Workflow engine on read. Thesummarize-textexample shows structured output through a forced tool call; an Agents page shows live timelines. - Retrieval — ingest text into
documents/chunks(paragraph-aware chunking, inline or queued indexing),vector(1024)embeddings with an HNSW index, and hybrid search: dense cosine + lexicaltsvector, fused with Reciprocal Rank Fusion. Vectors are ordinary tenant-scoped rows. - Document uploads — PDF, Word, Excel, OpenDocument, HTML and XML are stored in R2 and converted to Markdown by Workers AI (
env.AI.toMarkdown, free for documents) in adocument.convertjob, then indexed like pasted text; the original stays downloadable. Agents read the same knowledge base through built-insearch_knowledge/get_document/list_documentstools. - Usage ledger and tracing — one
ai_usagerow per model call with token counts and a usage summary endpoint; vendor-neutral OTLP traces with GenAI conventions (agent → model calls, tools, retrieval, embeddings) exported to Langfuse, Phoenix or any OTLP backend fromwaitUntil, and always recorded locally sorocketflare traces show <runId>works with zero config — no OpenTelemetry dependency. - Evals and feedback —
pnpm evalruns vitest-evals suites against the real chat route and agent runtime in-process, with deterministic judges (tool trajectory, schema, contains, budget) and LLM judges (rubric, faithfulness to what was retrieved, reference answer) that go through the kit's own model resolver; runs are JSON with committed baselines and--compareregression checks, plus a local report UI. Thumbs on chat replies and run output feedrocketflare evals promote, which turns a bad real answer into a draft case. The/rf-evalsskill drives all of it.
- Semantic layer — drizzle-cube mounted at
/cubejs-apiand/mcpbehind the app's auth; every cube scopes its SQL to the current tenant, and a mandatory isolation test queries every cube as two tenants and asserts disjoint rows. - Fact tables — plain tables rebuilt per tenant in one transaction by the hourly cron, with a freshness endpoint and
rocketflare analytics check-factsfor ops. - Dashboards — TypeScript templates copied into each tenant's
analytics_pages(seeded on tenant creation and lazily on first read), editable in the UI with autosave, reset-to-template and recreate; an explore/query-builder page; a shipped "Organisation Overview" dashboard. - MCP — the same semantic layer is an MCP endpoint, so an AI client can query a tenant's analytics with a tenant API key.
rocketflare loginopens the browser, completes sign-in and tenant selection in the app, and receives a tenant API key on a loopback callback — stored0600in~/.rocketflare/config.json, never printed in full.whoami,status,members list,keys list,activity list,traces list|show(the AI span tree of a run or chat turn, admin+),feedback listandevals promote(turn a thumbs-down into an eval case, admin+),config;--jsonprints only the parsed response so output pipes intojq;ROCKETFLARE_API_KEY/ROCKETFLARE_URLreplace the config file in CI.- Every response is parsed with the same zod schema the server validated with; exit codes distinguish "not logged in" (2) and "forbidden" (3) from other errors (1).
- Contracts first —
packages/sharedholds the zod schemas the API validates with and the UI and CLI parse with; consumed as TypeScript source, no build step. - Two environments, one shape —
wrangler.tomlandwrangler.staging.tomlkept identical in everything code can observe by a parity test; account-scoped names suffixed-staging. - Release dance — tag = root version → staging deploys; publish the GitHub Release → production ships the same tag. Migrations run in CI against the environment's Neon branch before deploy. Or set
DEPLOYER_URLand an external deployer ships it instead, so CI holds no Cloudflare token and no production database credential (docs/DEPLOYER.md). - Tests that mean something — API tests drive the real Hono app against a real Postgres; queue consumers, Workflow steps, the Durable Object and cron tasks are plain functions tested directly; UI tests in jsdom; a config project checks tomls, permissions and dashboard templates with no database.
- Agent-readable —
CLAUDE.md(alsoAGENTS.md), path-scoped rules in.claude/rules/, aCLAUDE.mdin every significant directory, anddocs/CONCEPTS.mddescribing each subsystem, its invariant and its known gaps. - Plugins — a feature can be a separate git repository copied in (never npm-installed, exactly like the kit): six barrels take one line each, and a plugin contributes contracts, tables with RLS policies, routes, jobs, agent tools, lifecycle hooks, Durable Object and Workflow classes, lazy pages, nav items and CLI commands without a core file naming it. In the other direction it imports the kit only through declared entries — a family of injected execution contexts, versioned as
PLUGIN_APIand documented in a generated, diff-checkeddocs/plugin-api.md— so the plugin surface can stand still while kit internals move.pnpm plugin checkis an exhaustive audit whose every finding carries the file, the line and the exact edit, and CI runs the same command;example-featureis the vendored reference plugin, and it is meant to be deleted. - Design tokens — two DaisyUI themes whose brand values live in one header block; a contrast test gates the emitted tokens.
rocketflare/ workspace root: package.json (scripts delegate via pnpm -r / --filter),
│ pnpm-workspace.yaml, biome.json, tsconfig.base.json, CLAUDE.md, docs/, .github/
├── apps/web/ @rocketflare/web — Worker (Hono API) + React UI; wrangler*.toml, migrations/, scripts/, tests/
├── apps/cli/ @rocketflare/cli — `rocketflare` CLI: login, logout, whoami, status, members/keys/activity list, traces list|show, feedback list, evals promote, config
├── apps/evals/ @rocketflare/evals — developer-run eval suites (vitest-evals): `pnpm eval`, never part of the gate
└── packages/shared/ @rocketflare/shared — PRIVATE zod contracts, error envelope, pagination, permission types;
consumed as TypeScript source through the workspace link (no build step)
Everything runs from the root: pnpm bootstrap (= bash scripts/bootstrap.sh once Node and pnpm
exist; --offline / --online toggle the [ai] block), pnpm preflight (the read-only check),
pnpm dev, pnpm seed / pnpm seed --demo, pnpm test, pnpm cli …, pnpm web <script> (any
apps/web script), pnpm db:*, pnpm deploy[:staging], pnpm provision tokens (hidden prompts → apps/web/.provision.env)
then pnpm provision all (the deploy orchestrator: Neon, Cloudflare, GitHub, secrets, Resend —
pnpm provision --help lists the phases;
pnpm web provision:cloudflare <env> --apply is its Cloudflare-resources half). wrangler is a
devDependency of apps/web, so it is pnpm --filter @rocketflare/web exec wrangler …, never pnpm exec wrangler at the root. Root scripts/ holds the first-run tooling: bootstrap.sh / bootstrap.mjs,
rename.mjs (docs/ADAPTING.md §1 as one command) and their lib/ — plus, in the kit repository
only, install.sh and the release and maintainer-hook scripts, which the rename deletes.
Billing and subscriptions, Vectorize (vectors live in pgvector under the tenant predicate), OCR of
images (document conversion covers PDF/Office/HTML, not pictures), reranking, prompt versioning,
reporting/export, and any product domain. Each is a documented extension point in docs/CONCEPTS.md; the subsystem sections
there list every known gap.
| File | Read it when |
|---|---|
CLAUDE.md (AGENTS.md) |
always — the canonical agent context: stack, commands, map, non-negotiables |
SETUP.md |
getting a clone running, the CLI's first login, configuring OAuth/email/AI providers (or a local OpenAI-compatible mock)/tracing, deploying to Cloudflare |
docs/CONCEPTS.md |
before assuming a capability exists or building a new one — one section per subsystem with its invariant and known gaps |
docs/ADAPTING.md |
you just copied the kit to start an app (package names, CLI bin, config dir, env prefix, what to delete, where the first features go) |
docs/DEPLOY.md |
Cloudflare topology, the two tomls, resources, release dance, rollback, bundle size |
docs/DEPLOYER.md |
you are building a service that deploys kit apps from CI (the versioned v1 protocol behind DEPLOYER_URL) |
docs/RLS.md |
tenant isolation posture and how to turn row-level security on |
.claude/rules/*.md |
layer conventions (api, database, ui, cli, testing, code-quality, cloudflare) — auto-loaded by path |
.claude/skills/ |
the slash commands a coding agent drives: /rf-setup (first run), /rf-preflight (read-only diagnosis), /rf-adapt (rename + checklist), /rf-how-do-i (coaching for a new feature — asks, plans, writes docs/features/<slug>.md, never the code), /rf-upgrade (port later kit releases into your copy), /rf-plugin (install, upgrade, remove or audit a plugin), /rf-traces (debug a run or chat turn from its span tree; pick or switch a tracing backend), /rf-evals (write, run and interpret evals; compare models or prompts; turn thumbs-down into cases), /rf-agent-replay (replay an agent's past runs with the code you just changed — recorded answers, no model calls) — an agent may run those when you ask in plain words — and /rf-provision (deploy to Cloudflare + Neon + Resend), which only you can start: it creates paid resources and prompts for tokens |
Extracted from two internal applications: one contributed the structure, docs system, auth,
tenancy and AI layer; the other the Cloudflare substrate (Hyperdrive, Queues, Workflows, Durable
Objects, two-toml deploys) and the analytics layer. The decisions those two informed are recorded,
subsystem by subsystem, in docs/CONCEPTS.md.
To the kit itself (an app made from it loses CONTRIBUTING.md and CODE_OF_CONDUCT.md at the
rename, and gets its own SECURITY.md): bug reports, feature requests and pull requests are
welcome — see CONTRIBUTING.md
(setup, the pre-commit gate, test shapes, PR guidelines), CODE_OF_CONDUCT.md
and SECURITY.md for reporting vulnerabilities privately.
MIT.