AI Guardrails
AI agents are fast, but they don't understand consequences. An agent can generate a currency conversion function in 10 seconds — violating a billing invariant that took weeks to establish. It can put business logic in a controller because it saw that pattern in a tutorial. It can skip tenant scoping because the code "works" without it in development.
Guardrails are the solution. They are explicit, machine-readable rules embedded in configuration files and skills that tell AI agents what they must never do. Every guardrail exists because someone (or some agent) made that mistake before. They are preventive, not corrective — the agent reads them before writing code, not after breaking something.
SaaS4Builders ships guardrails for every critical domain: billing, architecture, tenancy, API contracts, and frontend structure. This page documents the pattern and shows you how to write guardrails for your own features.
The Guardrail Pattern
Every guardrail section in the project follows the same structure:
## Critical Rules
- NEVER [action that breaks an invariant]
- NEVER [another forbidden action]
- ALWAYS [required practice]
- ALWAYS [another required practice]
## Common Mistakes
- [Mistake description] — [why it happens]
- [Another mistake] — [what goes wrong]
Three design principles make this pattern effective:
- "NEVER" before "ALWAYS" — Agents learn constraints before affordances. Knowing what you cannot do is more important than knowing what you can do, because violations are harder to detect than omissions.
- One rule per line — Each rule is atomic and scannable. No paragraphs, no ambiguity. An agent can process the entire list in one pass.
- Common Mistakes section — Proactive error prevention. Instead of waiting for the agent to make a mistake and then correcting it, the guardrails describe the mistake pattern so the agent avoids it from the start.
The strongest guardrails add an enforcement clause:
Failure to comply invalidates generated code.
This tells the agent that code violating these rules should not be written at all — not written and then fixed, but prevented entirely.
Billing Guardrails
Billing has the most comprehensive guardrail set in the project. This is intentional — billing bugs have legal, financial, and compliance consequences that are disproportionately expensive to fix.
Core Invariants
Five rules that apply regardless of billing mode or version:
| Rule | What It Prevents |
|---|---|
| One currency per invoice | Mixed-currency line items, incorrect totals |
| Subscription currency is immutable | Currency changing on plan upgrades or admin edits |
| No cross-currency arithmetic | Adding EUR to USD, comparing across currencies |
| No FX conversion | Exchange rate logic, "display currency" features |
| Plan must exist in subscription currency | Fallback to wrong currency, implicit conversion |
These are implemented as domain exceptions. If code attempts to violate any of these rules, the system throws an exception rather than silently proceeding:
public function add(Money $other): self
{
$this->assertSameCurrency($other);
return new self($this->amount + $other->amount, $this->currency);
}
private function assertSameCurrency(Money $other): void
{
if (! $this->isSameCurrency($other)) {
throw CurrencyMismatchException::forOperation(
$this->currency,
$other->currency
);
}
}
The guardrails in the billing skill mirror these invariants:
## Critical Rules — V1 Invariants
- NEVER generate authoritative invoices internally — Stripe generates all invoices in V1
- NEVER treat shadow calculations as the billing source of truth
- NEVER mutate subscription state without a webhook trigger
- NEVER call Stripe SDK directly from Actions — use Domain Contracts
- NEVER store money as floats — always `amount_cents` (int) + `currency` (string)
- NEVER allow FX conversion — one currency per invoice, immutable
V1-Specific Rules
In V1, the billing system operates in stripe_managed mode — Stripe is the invoicing authority for all pricing types. The internal billing engine runs as a shadow for validation and future migration, but it is not authoritative:
| V1 Rule | Reason |
|---|---|
| Stripe Invoice is the source of truth | Internal invoices are shadow copies, not authoritative |
| Invoice data syncs FROM Stripe via webhooks | Never generate invoices internally |
| Lifecycle state changes happen via webhooks only | Never mutate subscription state from internal logic |
| Shadow calculations are validation only | Never display internal calculations to users |
Intentional Limitations
These are not missing features — they are deliberate design decisions. AI agents must not attempt to implement them:
| Limitation | Status |
|---|---|
| Refunds | Not supported in V1 |
| Credit notes / adjustments | Not implemented |
| Usage-based billing | Model ready, not wired |
| Multi-provider billing | Stripe only |
| Manual invoice editing | Invoices are immutable |
| Dynamic tax overrides | Delegated to Stripe Tax |
| Partial-period first invoice | Full period billing only |
| Cross-currency operations | Forbidden by design |
The guardrails for these limitations are explicit:
AI agents must NOT:
- Implement refunds
- Modify invoices after creation
- Bypass domain guards
- Introduce FX logic
- Add provider abstractions beyond Stripe
The Billing Guardrails Skill
The project ships a dedicated billing-guardrails skill (for Codex) and a /docs/billing skill (for Claude Code) that consolidate all billing invariants into a single loadable context:
## Do Not Break
- Never generate authoritative invoices internally in V1.
- Never treat shadow calculations as the billing source of truth.
- Never mutate subscription state outside the webhook-driven lifecycle.
- One currency per invoice.
- Subscription currency is immutable.
- No FX conversion.
- A plan must exist in the subscription currency.
- Webhooks must stay idempotent.
- Actions must not call Stripe SDK directly; use contracts and providers.
- Money stays in integer minor units (amount_cents) only.
The skill also includes a "Read Before Coding" section that lists every document the agent should read before touching billing code, and a "Common Failure Modes" section that describes mistakes agents have actually made.
Backend Architecture Guardrails
The backend enforces a layered architecture: Controllers → Requests → DTOs → Actions/Queries → Domain → Models → Resources. Guardrails prevent agents from collapsing these layers.
Mandatory Rules
| Rule | What It Enforces |
|---|---|
| Thin Controllers | Controllers delegate only — no business logic, no query building |
| Request → toDto() | Every FormRequest exposes toDto() mapping HTTP input to an Application DTO |
| Actions = Mutations | Wrap in DB::transaction(), return Eloquent Model, never read-only |
| Queries = Read-only | No side effects, may use Eloquent/QueryBuilder |
| Domain Contracts | Actions depend on interfaces (PaymentGateway), never on providers (StripeProvider) directly |
| Mappers normalize external data | External responses are mapped to Domain DTOs before reaching Actions |
These rules live in backend/CLAUDE.md and are loaded automatically when an agent works in the backend directory.
PR Review Checklist
The backend configuration also includes a review checklist that agents apply before finalizing code:
### Architecture
- [ ] Controllers contain no business logic
- [ ] Every Request has toDto()
- [ ] Input DTOs in Application/*/DTO/, named *Data or *Filters
- [ ] Domain DTOs are provider-agnostic (no stripe_* fields)
- [ ] Actions mutate state with DB::transaction(); Queries do not mutate
- [ ] External calls go through Domain Contracts
### API Contracts Compliance
- [ ] Resource keys are snake_case
- [ ] Dates use ->toIso8601String() (ISO-8601)
- [ ] Money fields use amount_cents + currency
- [ ] Pagination returns { current_page, last_page, per_page, total }
This checklist pattern is effective because it gives the agent a concrete verification step — not just "follow the rules" but "check these specific things."
Frontend Architecture Guardrails
The frontend enforces vertical slice isolation and strict API patterns.
Layer Dependency Rules
The feature directory has three layers with strict import rules:
product/ → can import → core/, foundation/, common/
core/ → can import → foundation/, common/
foundation/ → can import → common/ only
common/ → imports NOTHING from features/
Violations the agent must reject:
common/importing fromfeatures/*foundation/importing fromcore/orproduct/core/importing fromproduct/- Cross-imports between core features (except
catalogwhich is an owner)
API Call Pattern
Composables never call $fetch or useFetch directly. All data fetching flows through the API module layer:
Page → Composable → useXxxApi() → common/api/client.ts
// WRONG — violates the API call guardrail
const { data } = useFetch('/api/v1/subscription')
// CORRECT — uses the authenticated data wrapper
const { data } = useAuthenticatedAsyncData(
'billing:subscription',
() => billingApi.getSubscription()
)
The useAuthenticatedAsyncData wrapper exists because Sanctum auth cookies are not available during SSR. Raw useAsyncData without server: false will silently return null on page refresh — a bug that is invisible during development but breaks production.
Missing Endpoint Rule
If an API endpoint does not exist in the route file or is not documented:
→ DO NOT invent it.
→ Propose the endpoint shape and stop.
This prevents agents from generating frontend code that calls non-existent endpoints — a common mistake when an agent "knows" what endpoint should exist but the backend hasn't implemented it yet.
Tenant Isolation Guardrails
Tenant isolation is a cross-cutting concern that appears in both backend and frontend guardrails.
Backend Rules
| Rule | Implementation |
|---|---|
Every tenant-scoped model uses BelongsToTenant | Trait adds global scope + auto-sets tenant_id |
| Test isolation on every scoped resource | List test: 2 tenants, assert only own data. Detail test: other tenant's resource returns 404 |
Cache keys include tenant_id | Prevents cross-tenant cache poisoning |
| Background jobs receive tenant context explicitly | Tenant is not available in queue context |
Compound index on ['tenant_id', 'created_at'] | Performance for tenant-scoped time-ordered queries |
use App\Models\Concerns\BelongsToTenant;
class Resource extends Model
{
use BelongsToTenant;
use HasFactory;
}
The trait handles three things automatically: a global scope that filters all queries by tenant_id, an observer that sets tenant_id on creation, and a tenant() relationship.
Frontend Rules
The frontend resolves tenant context through a composable:
const { tenantId, currentTenant } = useCurrentTenant()
const path = tenantPath('/team/members')
// → /api/v1/tenant/{tenantId}/team/members
All tenant-scoped API calls go through tenantPath(), which prepends the resolved tenant ID. The guardrail is simple: never hardcode a tenant ID in a URL.
API Contract Guardrails
The API contract is the interface between backend and frontend. Guardrails ensure both sides stay synchronized.
Format Rules
| Contract | Rule | Example |
|---|---|---|
| Casing | All JSON keys are snake_case | created_at, amount_cents, tenant_id |
| Dates | ISO-8601 UTC format | 2025-12-26T14:35:21Z |
| Money | Integer minor units + currency code | { "amount_cents": 1299, "currency": "EUR" } |
| IDs | UUIDs for public identifiers | "550e8400-e29b-41d4-a716-446655440000" |
| Booleans | Strict JSON booleans | true / false (never "true") |
| Enums | Lowercase string values | "active", "trialing", "canceled" |
| Lists | Wrapped in data key | { "data": [] } |
| Pagination | Standard meta object | { "current_page": 1, "last_page": 1, "per_page": 25, "total": 0 } |
The Undocumented Endpoint Rule
This rule appears in both CLAUDE.md and frontend/CLAUDE.md:
Undocumented endpoint? Propose the shape first, do not implement.
This is one of the most frequently triggered guardrails. When an agent needs an endpoint that doesn't exist, the natural instinct is to create it. The guardrail forces a pause — the agent proposes the endpoint shape for human review before any code is written. This prevents misaligned contracts between backend and frontend.
Writing Guardrails for Your Own Features
When you add a new domain to the project, write guardrails before you write code. Here is the process:
Step 1 — Identify Domain Invariants
Ask: "What can never happen in this domain?" These are your invariants — the rules that, if violated, produce incorrect behavior that is hard to detect.
For example, if you're building a notifications domain:
- A notification can never be sent twice (idempotency)
- A dismissed notification can never reappear
- Push tokens must be validated before sending
- Rate limits must be enforced per user, not globally
Step 2 — Write NEVER/ALWAYS Rules
Convert each invariant to an explicit rule:
## Critical Rules
- NEVER send a notification without checking idempotency key
- NEVER re-surface a dismissed notification
- NEVER send push to an unvalidated token
- NEVER apply rate limits globally — always per-user
- ALWAYS record delivery status (sent, failed, bounced)
- ALWAYS validate push tokens on registration and refresh
Step 3 — Add Common Mistakes
Think about how an agent (or developer) might violate each rule:
## Common Mistakes
- Using the notification ID as the idempotency key (should be a separate business key)
- Querying dismissed notifications without filtering — returns everything including dismissed
- Catching push delivery errors silently instead of recording failure status
- Rate limiting by IP address instead of user ID in multi-tenant context
Step 4 — Place the Guardrails
Add your guardrails to three locations:
| Location | What To Add | Purpose |
|---|---|---|
CLAUDE.md (root or backend/frontend) | One-line critical rules in the "Do NOT" section | Loaded automatically on every session |
.claude/skills/<domain>/SKILL.md | Full guardrail section with context | Loaded when agent works on this domain |
.agents/skills/<domain>/SKILL.md | Codex equivalent of the skill | Same guardrails for Codex agents |
If your domain has complex invariants (like billing), also create a dedicated guardrail skill:
| File | Content |
|---|---|
.claude/skills/<domain>/SKILL.md | Domain skill with guardrails as one section |
.agents/skills/<domain>-guardrails/SKILL.md | Standalone guardrail skill (Codex) |
Step 5 — Write Domain Documentation
Create a document in docs/<domain>/ that explains the architecture and embeds the full guardrail section with the "Mandatory Rules for AI Agents" header. This document becomes the source of truth that both skills reference.
Where Guardrails Live
A summary of every guardrail location in the project:
| File | Guardrail Domain | Scope |
|---|---|---|
CLAUDE.md | All domains (summary) | Root — always loaded |
backend/CLAUDE.md | Backend architecture | Loaded when working in backend/ |
frontend/CLAUDE.md | Frontend architecture | Loaded when working in frontend/ |
.claude/skills/docs/billing/SKILL.md | Billing (Claude Code) | Loaded via /docs/billing skill |
.claude/skills/docs/multi-tenancy/SKILL.md | Tenancy (Claude Code) | Loaded via /docs/multi-tenancy skill |
.claude/skills/api-contracts/SKILL.md | API format (Claude Code) | Loaded via /api-contracts skill |
.agents/skills/docs/billing-guardrails/SKILL.md | Billing (Codex) | Loaded via Codex skill system |
docs/billing/BILLING.md | Billing architecture | Source of truth — referenced by skills |
docs/billing/CURRENCY-RULES.md | Currency invariants | Source of truth — 5 non-negotiable rules |
docs/billing/LIMITATIONS.md | V1 limitations | Source of truth — intentional constraints |
docs/api/CONTRACTS.md | API contracts | Source of truth — response format rules |
CLAUDE.md and AGENTS.md contain one-line summaries. Skills contain the working set of rules with context. Documentation files contain the full rationale and edge cases. An agent working on a quick fix reads the summary. An agent building a new feature loads the skill. An agent debugging a billing issue reads the full documentation.What's Next
- Convention Files — The six documents that define code patterns alongside these guardrails
- Context Management — How guardrails are loaded at the right time through progressive disclosure
- Skills System — How skills package guardrails as loadable context capsules
- Billing Overview — The billing system these guardrails protect
- Multi-Tenancy Overview — The tenancy system and isolation rules
Convention Files
The six convention documents that keep AI agent output consistent: API contracts, backend templates, frontend templates, query builder guide, domain glossary, and decision log.
Context Management
How the project manages AI agent context: the progressive disclosure model, skills as context capsules, lean context strategies, and performance implications for limited context windows.