Skip to content

feat: add multi-tenant SaaS template (--multi-tenant) - #133

Merged
PAMulligan merged 1 commit into
mainfrom
feature/multi-tenant-template
Jul 8, 2026
Merged

feat: add multi-tenant SaaS template (--multi-tenant)#133
PAMulligan merged 1 commit into
mainfrom
feature/multi-tenant-template

Conversation

@PAMulligan

Copy link
Copy Markdown
Contributor

Summary

Adds a multi-tenant SaaS template that extends the standard project setup with tenant isolation patterns. Generate with:

./scripts/setup-project.sh my-saas --node --multi-tenant   # composes with any platform flag

Isolation strategies (selectable via TENANCY_STRATEGY)

  • row (default): shared tables with a tenant_id FK on every resource table, a tenant-scoped query facade (createTenantDb) that injects the tenant predicate into every query, and PostgreSQL Row Level Security as a fail-closed backstop (withTenant() binds app.tenant_id via SET LOCAL; the policy template ships at src/db/rls-policies.sql).
  • schema: one PostgreSQL schema per tenant, switched with transaction-scoped search_path (withTenantSchema()), plus schema creation for onboarding and a documented per-tenant migration runner.

Tenant resolution middleware

tenantResolver resolves the tenant per request from, in configurable priority order:

  1. JWT claim tenant_id (signed — default first)
  2. Subdomain (acme.api.example.com → slug acme)
  3. X-Tenant-ID header (documented as spoofable / gateway-only)

Unresolvable → 400; unknown tenant → 404 (prevents tenant enumeration).

Schema additions

tenants table (id, name, slug, plan, created_at), a tenantScopedColumns() helper for resource tables, and an example projects table with the tenant_id index. The generated drizzle.config.ts includes the tenancy schema so drizzle-kit generate picks it up.

Tests

api/tests/tenancy.test.ts: resolution and SQL-scoping tests run with no database; setting TENANCY_TEST_DATABASE_URL activates a live suite that proves RLS visibility per tenant, fail-closed behavior without tenant context, WITH CHECK blocking cross-tenant writes/updates/deletes, and search_path schema isolation — running as a non-superuser role, in a throwaway namespace cleaned up afterwards.

Docs

docs/api-development/multi-tenancy.md documents the trade-offs between the two strategies (tenant count, migrations, cross-tenant analytics, backup/restore, noisy neighbors, compliance optics), when to pick which, and pooling/SET LOCAL caveats. Linked from the standards README, root README, and CLAUDE.md.

CI

  • templates/multi-tenant added to template validation
  • New node-multi-tenant generated-projects matrix entry that generates with the flag, type-checks, and runs the tenant-isolation tests

Test plan

  • Snippets type-check under all three target tsconfigs (Cloudflare / Node / Lambda)
  • bash -n + shellcheck (severity=warning) clean on setup-project.sh
  • --dry-run previews the tenancy files/appends correctly
  • Generated a real --multi-tenant project: pnpm typecheck passes, 51 tests pass (live suite skipped without DB)
  • Live suite: all 6 isolation tests pass against PostgreSQL 17
  • ci.yml YAML validates

Notes

While verifying, found a pre-existing issue (not introduced or fixed here): setup-project.sh fails on pnpm 11 for every platform flag with ERR_PNPM_IGNORED_BUILDS (esbuild build scripts). CI is unaffected (repo pins pnpm@10.15.1). Worth a follow-up to pre-approve builds in generated projects.

🤖 Generated with Claude Code

Extends setup-project.sh with a --multi-tenant flag (composable with any
platform target) that scaffolds tenant isolation:

- src/tenancy/ modules: strategy config (TENANCY_STRATEGY=row|schema),
  tenants registry schema + tenantScopedColumns() helper, tenant
  resolution middleware (JWT claim, subdomain, X-Tenant-ID header),
  tenant-scoped query facade + SET LOCAL app.tenant_id helper (row
  strategy), and per-tenant schema + search_path helpers (schema
  strategy)
- Row Level Security policy template (fail-closed FOR ALL policy per
  tenant-scoped table), shipped as src/db/rls-policies.sql to apply via
  a custom Drizzle migration
- Tenant-isolation test suite: resolution + SQL scoping tests run
  everywhere; an opt-in live PostgreSQL suite (TENANCY_TEST_DATABASE_URL)
  proves RLS visibility, WITH CHECK enforcement, and search_path
  isolation as a non-superuser role
- docs/api-development/multi-tenancy.md documenting the trade-offs
  between row-based and schema-based isolation and usage of both
- CI: validate templates/multi-tenant and add a node-multi-tenant
  generated-project job that also runs the tenancy tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added area: scripts Automation scripts area: templates Starter templates area: ci-cd CI/CD workflows area: docs Documentation labels Jul 8, 2026
@PAMulligan PAMulligan self-assigned this Jul 8, 2026
@PAMulligan
PAMulligan merged commit a068b72 into main Jul 8, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ci-cd CI/CD workflows area: docs Documentation area: scripts Automation scripts area: templates Starter templates

Projects

Development

Successfully merging this pull request may close these issues.

1 participant