Before you run this skill
This skill is brand-neutral. It reads its brand, palette and endpoints from
brand.config.json at the repo root.
On first use, do this before anything else:
- Run
python3 brandkit.py. It prints the config source and any placeholder
that is still unset.
- If it says
configured: False, copy brand.config.example.json to
brand.config.json.
- Ask the operator for each value under
missing, then write them in. Do not
guess a brand name, a domain, or a colour.
- Anything the skill writes out should be passed through
brandkit.fill(text), which swaps every {{TOKEN}} for its configured value
and remaps the default palette to the operator's.
Text below uses {{TOKEN}} where a value is operator-specific. Treat an
unresolved {{TOKEN}} in your output as a bug, not as literal copy.
SaaS Guardrails
The engineering contract for a multi-tenant SaaS, in any language.
The one idea behind everything here
A rule that executes beats a rule that must be remembered.
Prose rules decay. An agent reads "remember to check permissions" on turn 3 and forgets it on turn 40. So every rule here is placed as close as possible to a mechanism that fails loudly: a thrown error, a type error, a failing test, a database constraint. Where a rule genuinely cannot be mechanized it is marked [JUDGMENT] and belongs in a review, not in an agent's memory.
Two layers
This skill is deliberately split, because a stack-agnostic rule that names no mechanism is just advice, and advice is what fails.
| Layer |
What it is |
Where |
| Invariants |
Eight properties every build must have, in any language. No file names, no framework. |
references/11-invariants.md |
| Binding |
How this specific stack satisfies each invariant. Names the actual files, functions, and commands. |
bindings/<stack>.md |
The invariants never change. The binding changes per project. A project with no binding file is not yet governed by this skill: write the binding first, from bindings/_template.md.
Rule 0: Detect stack and mode before doing anything
# Which binding applies?
ls package.json pyproject.toml requirements.txt go.mod Gemfile Cargo.toml 2>/dev/null
test -f src/lib/resources.ts && echo "binding: nextjs-trpc-prisma"
test -f app/core/resources.py -o -f app/resources.py && echo "binding: python-fastapi-sqlalchemy"
ls bindings/ .claude/binding.md docs/binding.md 2>/dev/null
# Which mode?
ls prisma/migrations alembic/versions migrations 2>/dev/null | head -3
grep -rlE '"test"|pytest|vitest|jest' package.json pyproject.toml 2>/dev/null
| What you find |
Mode |
Load |
| Empty dir, no app yet |
Mode 1: New build |
references/01-discovery-and-gates.md, then pick a binding |
| A binding matches and the user wants a feature |
Mode 2: Add a feature |
the binding, then references/03-add-a-feature.md |
| An app exists that satisfies no binding |
Mode 3: Adopt |
references/10-adopting-into-existing-code.md |
| A diff or PR to judge |
Mode 4: Review |
checklists/feature-done.md |
| A new person joining |
Mode 5: Onboard |
checklists/new-developer-30-min.md |
| A stack with no binding file yet |
Mode 6: New binding |
bindings/_template.md |
Never guess. If the signals conflict, ask which applies.
The eight invariants (short form)
Full statements, rationale, and per-stack enforcement in references/11-invariants.md.
- Registry. One file declares every feature: its permissions, plan limit or flag, nav entry, audit template, rate limit. Feature metadata lives nowhere else.
- Chokepoint. One function or middleware wraps every application request and applies auth, tenancy, authorization, plan gates, transaction, usage counter, audit, and logging. Handlers contain business logic only.
- Tenancy in one place, checked twice. The chokepoint proves the caller belongs to the tenant. Every query proves the row does.
- Feature-oriented layout. Code is grouped by domain, not by technical type. Shared code is shared because two features use it, not because it might be reused someday.
- One type source of truth. The database's generated types first, then the auth library's, then a single types module. Never a second hand-written copy of a shape the database already describes.
- Grep index. Every authoritative export carries a keyword header so the next agent finds it in one search instead of reading files until it stumbles on the answer.
- Chokepoint tests are mandatory. The more code depends on something, the less optional its test. Everything depends on invariants 1 to 3, so those are tested first and stay green.
- Migrations only. Schema changes are versioned, reviewed, committed, and applied by a command. Never by hand, never by a sync-the-schema tool against anything shared.
Hard rules (every mode, every stack)
Ordered by how expensive the mistake is to undo. Where a rule names a mechanism, your binding says which one.
- No implementation before both gates pass. Discovery answered, architecture approved in writing. Being told to "just start coding" does not open the gate. See
01-discovery-and-gates.md.
- Tenancy is not optional. Every application table carries the tenant key. Every application read and write is scoped by it. Never fetch a tenant row by id alone.
- Every app endpoint goes through the chokepoint. No bare handlers for application data. Only exceptions: signature-verified third-party webhooks, the auth library's own routes, and health checks.
- Registry before code. A feature is declared before its handler exists. In a binding where an unregistered path throws, do not silence that error with a token override.
- Types have one home. Never redeclare a shape the database already describes. The banned-construct list is per language and lives in your binding.
- Schema validation on every input. Every endpoint, form, and webhook payload, with a max length on every string and array. No hand-rolled validation.
- The database layer is a boundary. Data access is isolated, and business logic does not live inside it. Your binding names the mechanism that enforces the boundary.
- Grep before you create. Search the keyword index first. Duplicating something that exists is the most common and most expensive failure in AI-assisted work.
- Every authoritative export carries a keyword header. Format in
05-types-and-navigation.md. This is what makes rule 8 cheap.
- Tests ship with the feature. The chokepoint suite in
06-testing.md is mandatory and stays green. A feature with no test is not done.
- Migrations only. Never a schema-sync command against anything shared. Every foreign key and every filtered column gets an index. See
07-database.md.
- Latest stable, never pre-release, always locked. No alpha, beta, RC, or nightly. Lockfile committed.
- Secrets never touch git. Environment validated at boot so a missing key fails at startup, not at 2am under load.
- One feature at a time, finished. Scope, contract, schema, endpoint, UI, tests, run the tests, review, next. Never ten half-features.
- No scripts, seeds, or probe files without explicit permission. Everything in the repo must be shippable.
- Design tokens only in UI code. No hex, no hardcoded colors, no forced theme classes.
- Report honestly. Run the typecheck and the tests and paste the real output. Never claim green without running it.
- Say what you left out. Name loose ends before handing over, including ones the user never asked about.
Rules we deliberately rejected
Recorded so no future session reintroduces them. All three come from the original guardrail doc.
| Rejected |
Why |
| "Max 250 lines per file" |
The chokepoint is one continuous request lifecycle. Splitting it to satisfy a number fragments the one story you most need to read end to end. Cohesion is the rule; length is a symptom worth a look, not a limit. |
| "Frontend and backend in separate repositories by default" |
One repo, always, for one product. Separate repos break shared types where the stack has them, and double the pipeline where it does not. Note this rejects separate repos, not backend/ and frontend/ folders, which are correct for a polyglot build. See the layout section of 11-invariants.md. |
| "Default stack is React + FastAPI + PostgreSQL for everything" |
A stack is chosen per project against the discovery answers, then bound. Mandating a polyglot split for a product that does not need one buys a hand-maintained API contract for nothing. |
The stack gate is kept. It just starts from whatever this project's binding is and asks whether there is a concrete reason to deviate.
Bindings
| Binding |
Use when |
bindings/nextjs-trpc-prisma.md |
One Next app, tRPC, Prisma, Better Auth. The strongest enforcement of the three, because the type chain is unbroken end to end. |
bindings/python-fastapi-sqlalchemy.md |
Python API plus a separate frontend. Use when the work is Python-shaped (data, ML, pipelines) or a constraint mandates it. |
bindings/_template.md |
Any other stack. Fill every invariant before writing product code. An unfilled invariant is a hole, and the template makes you name it. |
Reference files
Load on demand, never all at once. Files 02 through 09 are written against the Next binding and are the most detailed worked examples in the skill. When on another binding, read them for the reasoning and take the mechanism from your binding file.
| File |
When |
references/11-invariants.md |
Read first on any new stack. The stack-neutral layer, plus folder layout. |
references/01-discovery-and-gates.md |
Starting a project or a large feature. |
references/02-architecture.md |
The mental model: layers, registry, chokepoint, request lifecycle. |
references/03-add-a-feature.md |
The most used file. Full worked example, schema to UI to tests. |
references/04-security-and-tenancy.md |
Anything touching auth, permissions, cross-tenant access, secrets, audit, rate limits. |
references/05-types-and-navigation.md |
Type hierarchy, banned constructs, keyword header format, grep-first workflow. |
references/06-testing.md |
What must be tested, the mandatory chokepoint suite. |
references/07-database.md |
Schema changes, migrations, indexes, transactions, quota correctness. |
references/08-deploy-and-ops.md |
CI/CD, env validation, health checks, rollback, logging. |
references/09-known-defects-and-fixes.md |
Eight known defects in the Web Prodigies starter kit with exact patches. Apply before shipping anything real on that binding. |
references/10-adopting-into-existing-code.md |
Mode 3. Retrofitting without a rewrite. |
Checklists
| File |
When |
checklists/feature-done.md |
Before calling a feature complete, and in every review. |
checklists/pre-deploy.md |
Before any deploy to a shared environment. |
checklists/new-developer-30-min.md |
Onboarding, and as the acceptance test for whether the architecture is still legible. |
The onboarding promise
A new developer reaches their first correct commit in under 30 minutes, without asking anyone where code lives. That is the acceptance test for the whole architecture, and it is checked with a real person rather than assumed.
If someone cannot answer "where does the billing webhook live" or "where do I add a permission" within a minute of reading the binding and 03-add-a-feature.md, the architecture has drifted or the docs are lying. Fix one of them, and say which.
1---2name: saas-guardrails3description: The stack-agnostic engineering contract for building and extending a multi-tenant SaaS. Defines eight invariants every build must satisfy (a feature registry, one request chokepoint, tenancy enforced in one place, feature-oriented folders, one type source of truth, a grep index, a mandatory chokepoint test suite, migrations-only schema change), then binds each invariant to a concrete mechanism through a per-stack binding file (Next+tRPC+Prisma, Python+FastAPI+SQLAlchemy, or a new one from the template). Merges the process gates from the architectural-guardrail doc (discovery, stack selection, architecture approval, one-feature-at-a-time, version policy, deployment automation) with the enforced runtime patterns from the Web Prodigies starter kit, and closes the eight holes both leave open (no tests, missing tenancy indexes, a quota race, swallowed audit failures, fail-open auth rate limiting, no baseline migration, unpluggable logging, a stale grep pointer). Use for: starting a new SaaS in any language, adding4---5<!-- SETUP:BEGIN -->6## Before you run this skill78This skill is brand-neutral. It reads its brand, palette and endpoints from9`brand.config.json` at the repo root.1011**On first use, do this before anything else:**12131. Run `python3 brandkit.py`. It prints the config source and any placeholder14 that is still unset.152. If it says `configured: False`, copy `brand.config.example.json` to16 `brand.config.json`.173. Ask the operator for each value under `missing`, then write them in. Do not18 guess a brand name, a domain, or a colour.194. Anything the skill writes out should be passed through20 `brandkit.fill(text)`, which swaps every `{{TOKEN}}` for its configured value21 and remaps the default palette to the operator's.2223Text below uses `{{TOKEN}}` where a value is operator-specific. Treat an24unresolved `{{TOKEN}}` in your output as a bug, not as literal copy.2526<!-- SETUP:END -->2728# SaaS Guardrails2930The engineering contract for a multi-tenant SaaS, in any language.3132## The one idea behind everything here3334**A rule that executes beats a rule that must be remembered.**3536Prose rules decay. An agent reads "remember to check permissions" on turn 3 and forgets it on turn 40. So every rule here is placed as close as possible to a mechanism that fails loudly: a thrown error, a type error, a failing test, a database constraint. Where a rule genuinely cannot be mechanized it is marked `[JUDGMENT]` and belongs in a review, not in an agent's memory.3738## Two layers3940This skill is deliberately split, because a stack-agnostic rule that names no mechanism is just advice, and advice is what fails.4142| Layer | What it is | Where |43| --- | --- | --- |44| **Invariants** | Eight properties every build must have, in any language. No file names, no framework. | `references/11-invariants.md` |45| **Binding** | How this specific stack satisfies each invariant. Names the actual files, functions, and commands. | `bindings/<stack>.md` |4647The invariants never change. The binding changes per project. A project with no binding file is not yet governed by this skill: write the binding first, from `bindings/_template.md`.4849## Rule 0: Detect stack and mode before doing anything5051```bash52# Which binding applies?53ls package.json pyproject.toml requirements.txt go.mod Gemfile Cargo.toml 2>/dev/null54test -f src/lib/resources.ts && echo "binding: nextjs-trpc-prisma"55test -f app/core/resources.py -o -f app/resources.py && echo "binding: python-fastapi-sqlalchemy"56ls bindings/ .claude/binding.md docs/binding.md 2>/dev/null5758# Which mode?59ls prisma/migrations alembic/versions migrations 2>/dev/null | head -360grep -rlE '"test"|pytest|vitest|jest' package.json pyproject.toml 2>/dev/null61```6263| What you find | Mode | Load |64| --- | --- | --- |65| Empty dir, no app yet | **Mode 1: New build** | `references/01-discovery-and-gates.md`, then pick a binding |66| A binding matches and the user wants a feature | **Mode 2: Add a feature** | the binding, then `references/03-add-a-feature.md` |67| An app exists that satisfies no binding | **Mode 3: Adopt** | `references/10-adopting-into-existing-code.md` |68| A diff or PR to judge | **Mode 4: Review** | `checklists/feature-done.md` |69| A new person joining | **Mode 5: Onboard** | `checklists/new-developer-30-min.md` |70| A stack with no binding file yet | **Mode 6: New binding** | `bindings/_template.md` |7172Never guess. If the signals conflict, ask which applies.7374## The eight invariants (short form)7576Full statements, rationale, and per-stack enforcement in `references/11-invariants.md`.77781. **Registry.** One file declares every feature: its permissions, plan limit or flag, nav entry, audit template, rate limit. Feature metadata lives nowhere else.792. **Chokepoint.** One function or middleware wraps every application request and applies auth, tenancy, authorization, plan gates, transaction, usage counter, audit, and logging. Handlers contain business logic only.803. **Tenancy in one place, checked twice.** The chokepoint proves the caller belongs to the tenant. Every query proves the row does.814. **Feature-oriented layout.** Code is grouped by domain, not by technical type. Shared code is shared because two features use it, not because it might be reused someday.825. **One type source of truth.** The database's generated types first, then the auth library's, then a single types module. Never a second hand-written copy of a shape the database already describes.836. **Grep index.** Every authoritative export carries a keyword header so the next agent finds it in one search instead of reading files until it stumbles on the answer.847. **Chokepoint tests are mandatory.** The more code depends on something, the less optional its test. Everything depends on invariants 1 to 3, so those are tested first and stay green.858. **Migrations only.** Schema changes are versioned, reviewed, committed, and applied by a command. Never by hand, never by a sync-the-schema tool against anything shared.8687## Hard rules (every mode, every stack)8889Ordered by how expensive the mistake is to undo. Where a rule names a mechanism, your binding says which one.90911. **No implementation before both gates pass.** Discovery answered, architecture approved in writing. Being told to "just start coding" does not open the gate. See `01-discovery-and-gates.md`.922. **Tenancy is not optional.** Every application table carries the tenant key. Every application read and write is scoped by it. Never fetch a tenant row by id alone.933. **Every app endpoint goes through the chokepoint.** No bare handlers for application data. Only exceptions: signature-verified third-party webhooks, the auth library's own routes, and health checks.944. **Registry before code.** A feature is declared before its handler exists. In a binding where an unregistered path throws, do not silence that error with a token override.955. **Types have one home.** Never redeclare a shape the database already describes. The banned-construct list is per language and lives in your binding.966. **Schema validation on every input.** Every endpoint, form, and webhook payload, with a max length on every string and array. No hand-rolled validation.977. **The database layer is a boundary.** Data access is isolated, and business logic does not live inside it. Your binding names the mechanism that enforces the boundary.988. **Grep before you create.** Search the keyword index first. Duplicating something that exists is the most common and most expensive failure in AI-assisted work.999. **Every authoritative export carries a keyword header.** Format in `05-types-and-navigation.md`. This is what makes rule 8 cheap.10010. **Tests ship with the feature.** The chokepoint suite in `06-testing.md` is mandatory and stays green. A feature with no test is not done.10111. **Migrations only.** Never a schema-sync command against anything shared. Every foreign key and every filtered column gets an index. See `07-database.md`.10212. **Latest stable, never pre-release, always locked.** No alpha, beta, RC, or nightly. Lockfile committed.10313. **Secrets never touch git.** Environment validated at boot so a missing key fails at startup, not at 2am under load.10414. **One feature at a time, finished.** Scope, contract, schema, endpoint, UI, tests, run the tests, review, next. Never ten half-features.10515. **No scripts, seeds, or probe files without explicit permission.** Everything in the repo must be shippable.10616. **Design tokens only in UI code.** No hex, no hardcoded colors, no forced theme classes.10717. **Report honestly.** Run the typecheck and the tests and paste the real output. Never claim green without running it.10818. **Say what you left out.** Name loose ends before handing over, including ones the user never asked about.109110## Rules we deliberately rejected111112Recorded so no future session reintroduces them. All three come from the original guardrail doc.113114| Rejected | Why |115| --- | --- |116| "Max 250 lines per file" | The chokepoint is one continuous request lifecycle. Splitting it to satisfy a number fragments the one story you most need to read end to end. Cohesion is the rule; length is a symptom worth a look, not a limit. |117| "Frontend and backend in separate repositories by default" | One repo, always, for one product. Separate repos break shared types where the stack has them, and double the pipeline where it does not. Note this rejects separate **repos**, not `backend/` and `frontend/` **folders**, which are correct for a polyglot build. See the layout section of `11-invariants.md`. |118| "Default stack is React + FastAPI + PostgreSQL for everything" | A stack is chosen per project against the discovery answers, then bound. Mandating a polyglot split for a product that does not need one buys a hand-maintained API contract for nothing. |119120The stack **gate** is kept. It just starts from whatever this project's binding is and asks whether there is a concrete reason to deviate.121122## Bindings123124| Binding | Use when |125| --- | --- |126| `bindings/nextjs-trpc-prisma.md` | One Next app, tRPC, Prisma, Better Auth. The strongest enforcement of the three, because the type chain is unbroken end to end. |127| `bindings/python-fastapi-sqlalchemy.md` | Python API plus a separate frontend. Use when the work is Python-shaped (data, ML, pipelines) or a constraint mandates it. |128| `bindings/_template.md` | Any other stack. Fill every invariant before writing product code. An unfilled invariant is a hole, and the template makes you name it. |129130## Reference files131132Load on demand, never all at once. Files 02 through 09 are written against the Next binding and are the most detailed worked examples in the skill. When on another binding, read them for the reasoning and take the mechanism from your binding file.133134| File | When |135| --- | --- |136| `references/11-invariants.md` | **Read first on any new stack.** The stack-neutral layer, plus folder layout. |137| `references/01-discovery-and-gates.md` | Starting a project or a large feature. |138| `references/02-architecture.md` | The mental model: layers, registry, chokepoint, request lifecycle. |139| `references/03-add-a-feature.md` | The most used file. Full worked example, schema to UI to tests. |140| `references/04-security-and-tenancy.md` | Anything touching auth, permissions, cross-tenant access, secrets, audit, rate limits. |141| `references/05-types-and-navigation.md` | Type hierarchy, banned constructs, keyword header format, grep-first workflow. |142| `references/06-testing.md` | What must be tested, the mandatory chokepoint suite. |143| `references/07-database.md` | Schema changes, migrations, indexes, transactions, quota correctness. |144| `references/08-deploy-and-ops.md` | CI/CD, env validation, health checks, rollback, logging. |145| `references/09-known-defects-and-fixes.md` | Eight known defects in the Web Prodigies starter kit with exact patches. Apply before shipping anything real on that binding. |146| `references/10-adopting-into-existing-code.md` | Mode 3. Retrofitting without a rewrite. |147148## Checklists149150| File | When |151| --- | --- |152| `checklists/feature-done.md` | Before calling a feature complete, and in every review. |153| `checklists/pre-deploy.md` | Before any deploy to a shared environment. |154| `checklists/new-developer-30-min.md` | Onboarding, and as the acceptance test for whether the architecture is still legible. |155156## The onboarding promise157158A new developer reaches their first correct commit in **under 30 minutes**, without asking anyone where code lives. That is the acceptance test for the whole architecture, and it is checked with a real person rather than assumed.159160If someone cannot answer "where does the billing webhook live" or "where do I add a permission" within a minute of reading the binding and `03-add-a-feature.md`, the architecture has drifted or the docs are lying. Fix one of them, and say which.