rocketflare-dev/rocketflare(opens in a new tab)
Language: TypeScript
Rocketflare — multi-tenant SaaS starter: Hono API + React UI on Cloudflare Workers, CLI, shared zod contracts
- stars
- 11
- watchers
- 0
- forks
- 2
- open issues
- 16
- Last updated
- 10/31/2026
Language: TypeScript
Rocketflare — multi-tenant SaaS starter: Hono API + React UI on Cloudflare Workers, CLI, shared zod contracts
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.sh
By 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 |
TENANCY_MODE=multi|single. Every domain row carries a tenant_id; single mode auto-joins users to the one organisation and hides the org switcher. Flip later with no migration.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.owner | admin | member per tenant plus a platform support role 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./admin for global admins: tenants, users, access requests; "enter" a customer tenant as support with a real membership row, so the single "must be a member" invariant never bends.TENANT_SCOPE_MODE=enforce (docs/RLS.md), with a catalog test that fails CI if a table is missed.OIDC_ISSUER is set: discovery, PKCE + nonce, id_token verified against the issuer's JWKS. AUTH_OIDC_ONLY makes it the only sign-in; logout follows the issuer's end_session_endpoint.__Host-session cookie, 7-day sliding TTL, one LATERAL query per request.gitleaks in CI.JOBS_QUEUE with 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.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.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.fetch with 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.StorageService seam: tenant-prefixed keys, bytes streamed through the Worker, an indexed files table, per-scope MIME and size limits, avatars wired end-to-end (upload UI → /api/files/:id with ETag/304).@cf/zai-org/glm-4.7-flash by 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.{{variables}}; tenants override them in Settings → Prompts and revert with one click.POST /api/agents/runs enqueues 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. The summarize-text example shows structured output through a forced tool call; an Agents page shows live timelines.documents/chunks (paragraph-aware chunking, inline or queued indexing), vector(1024) embeddings with an HNSW index, and hybrid search: dense cosine + lexical tsvector, fused with Reciprocal Rank Fusion. Vectors are ordinary tenant-scoped rows.env.AI.toMarkdown, free for documents) in a document.convert job, then indexed like pasted text; the original stays downloadable. Agents read the same knowledge base through built-in search_knowledge / get_document / list_documents tools.ai_usage row 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 from waitUntil, and always recorded locally so rocketflare traces show <runId> works with zero config — no OpenTelemetry dependency.pnpm eval runs 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 --compare regression checks, plus a local report UI. Thumbs on chat replies and run output feed rocketflare evals promote, which turns a bad real answer into a draft case. The /rf-evals skill drives all of it./cubejs-api and /mcp behind 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.rocketflare analytics check-facts for ops.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.rocketflare login opens the browser, completes sign-in and tenant selection in the app, and receives a tenant API key on a loopback callback — stored 0600 in ~/.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 list and evals promote (turn a thumbs-down into an eval case, admin+), config; --json prints only the parsed response so output pipes into jq; ROCKETFLARE_API_KEY / ROCKETFLARE_URL replace the config file in CI.packages/shared holds the zod schemas the API validates with and the UI and CLI parse with; consumed as TypeScript source, no build step.wrangler.toml and wrangler.staging.toml kept identical in everything code can observe by a parity test; account-scoped names suffixed -staging.DEPLOYER_URL and an external deployer ships it instead, so CI holds no Cloudflare token and no production database credential (docs/DEPLOYER.md).CLAUDE.md (also AGENTS.md), path-scoped rules in .claude/rules/, a CLAUDE.md in every significant directory, and docs/CONCEPTS.md describing each subsystem, its invariant and its known gaps.PLUGIN_API and documented in a generated, diff-checked docs/plugin-api.md — so the plugin surface can stand still while kit internals move. pnpm plugin check is an exhaustive audit whose every finding carries the file, the line and the exact edit, and CI runs the same command; example-feature is the vendored reference plugin, and it is meant to be deleted.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.