multi-tenant-saas
Reusable Cloudflare + Supabase multi-tenant SaaS starter, built as an Orun component-native desired-state repo. Identity, organizations, projects, RBAC, audit, metering, billing, webhooks, and notifications ship as separate bounded-context Cloudflare Workers behind a single public edge API, with a Next.js console on Workers + Static Assets.
Live deployment
Not yet recorded — run 08-docs after
06-console to fill this section from verified live state
(manifest · operating contract).
Status
- Runtime is live, per environment, through Orun. The edge API, the
bounded-context Workers, and the console deploy to
stageandprodviaorun run(no direct Wrangler/Terraform/pnpm in CI). - Data plane is provisioned by Terraform: Supabase
stageandprodprojects, Cloudflare Hyperdrive (pooled Postgres for Workers), and theapi-edgeidempotency KV namespace. Terraform state lives in the Orun Cloud HTTP state backend under this repo's workspace claim; provider credentials are BROKERED per run from the workspace's integration connections — no AWS, no long-lived secrets at rest. - Database migrations run through the
db-migratecomponent (plan on PRs, apply on merge tomain). - Billing is live end-to-end via the Polar adapter (embedded checkout, plan changes, multi-org fan-out).
- Known credential-blocked tails (see
specs/epics/saas-baseline/): full production OAuth/magic-link auth and Stripe require human-supplied credentials. The notifications email provider is Cloudflare Email Service (cloudflare-email, no API key — thesend_emailbinding is the credential); it needs one-time account setup: Workers Paid plan and the sending domain verified in Email Service (DKIM/SPF). - The
devenvironment is verify-only (no provisioned Supabase project by design).
Instantiating products
This baseline births new products from one artifact — repo-blueprint.yaml,
run by orun new (BOOTSTRAP.md →
docs/phases/). It declares ten idempotent phases —
scaffold, foundation, infrastructure, database, workers, workers-restore,
edge, console, optional domain, docs — each landing a verified slice, each
selectable with --phase <name>. An
operator needs two tokens and a workspace with Cloudflare and Supabase
connected; everything else is headless.
Products receive product-only content (source, infra, CI, their own
docs). None of this baseline's machinery ships — not the rebrand tooling,
not agents/, not ai/ state, not FORKING.md — and a product's docs speak
only about the product. testing/leak.test.sh and testing/placement.test.sh
hold that line: until they did, the scaffold shipped all four.
The mechanical identity rename (repo slug, product name/domain, SDK class,
CLI bin, worker prefixes, workers.dev subdomain, and the secret:// workspace
segment) is the blueprint's rebrand hook. What still needs human hands —
cloud accounts, OAuth apps, billing products — is the checklist in
FORKING.md.
Prerequisites
- Node.js >= 20 (CI and components run on Node 22)
- pnpm >= 10 (
npm install -g pnpm) - (Optional, for local Orun validation) the
kioxCLI on yourPATH.kioxpins the Orun provider declared inkiox.yaml; invoke Orun askiox -- orun ....
Getting Started
# Install all workspace dependencies
pnpm install
# Type-check / lint / test / build across the workspace (Turborepo)
pnpm typecheck
pnpm lint
pnpm test
pnpm build
Workspace Layout
apps/api-edge Public HTTP entry point (Cloudflare Worker)
apps/identity-worker Users, sessions, API keys, OAuth
apps/membership-worker Organizations, members, invitations, role assignments
apps/projects-worker Projects and environments
apps/policy-worker Deny-by-default RBAC evaluation
apps/events-worker Domain events, audit log, observability
apps/config-worker Settings, feature flags, secret metadata
apps/metering-worker Usage ingestion, quotas, rollups
apps/billing-worker Plans, subscriptions, invoices (Polar adapter)
apps/notifications-worker Email delivery and preferences
apps/webhooks-worker Outgoing webhooks: signing, delivery, replay
apps/admin-worker Audited admin/support workflows
apps/web-console-next Next.js console (Cloudflare Workers + Static Assets)
packages/contracts Shared API, tenancy, event, and error types + validators
packages/policy-engine RBAC evaluation logic
packages/db Migration harness, manifest, and runner
packages/sdk TypeScript SDK (contract-driven)
packages/cli `sourceplane` CLI
packages/notifications-client Notifications client
packages/shared Generic helpers (IDs, errors) — no domain logic
packages/testing Test fixtures and utilities
infra/terraform/supabase Supabase project provisioning (stage/prod)
infra/terraform/cloudflare-hyperdrive Hyperdrive config fronting Supabase
infra/terraform/cloudflare-kv api-edge idempotency KV namespace
infra/terraform/cloudflare-domain Zone adoption + console custom domain
infra/db-migrate Database migration runner component
tooling/tsconfig Shared TypeScript configurations
tooling/eslint Shared ESLint configuration
tooling/wire Deploy-time wrangler config renderer
tooling/docs Component docs renderer (CI checks it)
tests/* Per-component contract and verifier test suites
CI
CI is powered by Orun with the composition stack
consumed as a pinned OCI artifact (intent.yaml → compositions.sources). .github/workflows/ci.yml calls only orun plan and
orun run — no direct pnpm, turbo, Wrangler, or Terraform commands run in
GitHub Actions. The workflow's orun-action version: is the authoritative runtime pin.
GITHUB_TOKEN is the only credential CI holds: every provider credential is an
orun-managed secret, brokered fresh per run from the workspace's integration
connections and resolved lease-bound by the jobs that declare it.
Local Orun Verification
kiox -- orun compositions lock --intent intent.yaml
kiox -- orun validate --intent intent.yaml
kiox -- orun plan --changed --intent intent.yaml --output plan.json
kiox -- orun run --plan plan.json --dry-run --runner github-actions
Use --changed for PR-scoped checks; use a full plan when validating
environment promotion or cross-component dependencies (--view dag).
Infrastructure
Terraform provisions Supabase projects, Cloudflare Hyperdrive, and the
api-edge KV namespace for stage and prod. Terraform state lives in the
Orun Cloud HTTP state backend, scoped by the workspace claim in intent.yaml;
provider credentials are brokered per run from the workspace's Cloudflare and
Supabase connections. Each component publishes its resource identifiers as
workspace wiring secrets, which consumers resolve at deploy time — no resource
ID is ever committed. See specs/core/access-and-infra.md for the access
model and the manual prerequisites.
Adding a New Component
- Create the directory under
apps/,packages/,tests/, orinfra/. - Add a
component.yamlwith the appropriatespec.type— one ofcloudflare-worker-turbo,cloudflare-workers-assets-turbo,terraform,db-migrate, orturbo-package— plussubscribe.environmentsand the typedparametersthe composition schema requires. - Orun discovers it automatically on the next plan (
discovery.rootscoversapps/,infra/,packages/,tests/). Validate withkiox -- orun validate --intent intent.yaml.
See specs/core/orun-golden-path.md for the intent/component/composition layer
rules before changing CI, infra, or intent.yaml.
Build from the console
Press Build: Multi-tenant SaaS goes into the workspace you choose (or a new one), you connect Cloudflare and Supabase, and an agent builds it.
Build from the CLI
# check providers, then build
orun baseline check multi-tenant-saas
orun baseline new multi-tenant-saas --via-platform --repo acme/multi-tenant-saas