Skip to content
SaaS4Builders
Architecture for AI Agents

AI Guardrails

The mandatory rules pattern that prevents AI agents from breaking architectural invariants: billing, tenancy, API contracts, and how to write guardrails for your own features.

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:

  1. "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.
  2. One rule per line — Each rule is atomic and scannable. No paragraphs, no ambiguity. An agent can process the entire list in one pass.
  3. 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:

RuleWhat It Prevents
One currency per invoiceMixed-currency line items, incorrect totals
Subscription currency is immutableCurrency changing on plan upgrades or admin edits
No cross-currency arithmeticAdding EUR to USD, comparing across currencies
No FX conversionExchange rate logic, "display currency" features
Plan must exist in subscription currencyFallback 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:

backend/app/Domain/Shared/ValueObjects/Money.php
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 RuleReason
Stripe Invoice is the source of truthInternal invoices are shadow copies, not authoritative
Invoice data syncs FROM Stripe via webhooksNever generate invoices internally
Lifecycle state changes happen via webhooks onlyNever mutate subscription state from internal logic
Shadow calculations are validation onlyNever display internal calculations to users
The internal billing engine exists in the codebase and may appear functional. In V1, it is intentionally non-authoritative. An AI agent that reads the code without reading the guardrails might assume the internal engine is active and generate code that bypasses Stripe — producing a billing system that appears to work but generates incorrect invoices.

Intentional Limitations

These are not missing features — they are deliberate design decisions. AI agents must not attempt to implement them:

LimitationStatus
RefundsNot supported in V1
Credit notes / adjustmentsNot implemented
Usage-based billingModel ready, not wired
Multi-provider billingStripe only
Manual invoice editingInvoices are immutable
Dynamic tax overridesDelegated to Stripe Tax
Partial-period first invoiceFull period billing only
Cross-currency operationsForbidden 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:

.agents/skills/docs/billing-guardrails/SKILL.md
## 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

RuleWhat It Enforces
Thin ControllersControllers delegate only — no business logic, no query building
Request → toDto()Every FormRequest exposes toDto() mapping HTTP input to an Application DTO
Actions = MutationsWrap in DB::transaction(), return Eloquent Model, never read-only
Queries = Read-onlyNo side effects, may use Eloquent/QueryBuilder
Domain ContractsActions depend on interfaces (PaymentGateway), never on providers (StripeProvider) directly
Mappers normalize external dataExternal 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 from features/*
  • foundation/ importing from core/ or product/
  • core/ importing from product/
  • Cross-imports between core features (except catalog which 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

RuleImplementation
Every tenant-scoped model uses BelongsToTenantTrait adds global scope + auto-sets tenant_id
Test isolation on every scoped resourceList test: 2 tenants, assert only own data. Detail test: other tenant's resource returns 404
Cache keys include tenant_idPrevents cross-tenant cache poisoning
Background jobs receive tenant context explicitlyTenant is not available in queue context
Compound index on ['tenant_id', 'created_at']Performance for tenant-scoped time-ordered queries
backend/app/Models/Concerns/BelongsToTenant.php
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

ContractRuleExample
CasingAll JSON keys are snake_casecreated_at, amount_cents, tenant_id
DatesISO-8601 UTC format2025-12-26T14:35:21Z
MoneyInteger minor units + currency code{ "amount_cents": 1299, "currency": "EUR" }
IDsUUIDs for public identifiers"550e8400-e29b-41d4-a716-446655440000"
BooleansStrict JSON booleanstrue / false (never "true")
EnumsLowercase string values"active", "trialing", "canceled"
ListsWrapped in data key{ "data": [] }
PaginationStandard 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:

LocationWhat To AddPurpose
CLAUDE.md (root or backend/frontend)One-line critical rules in the "Do NOT" sectionLoaded automatically on every session
.claude/skills/<domain>/SKILL.mdFull guardrail section with contextLoaded when agent works on this domain
.agents/skills/<domain>/SKILL.mdCodex equivalent of the skillSame guardrails for Codex agents

If your domain has complex invariants (like billing), also create a dedicated guardrail skill:

FileContent
.claude/skills/<domain>/SKILL.mdDomain skill with guardrails as one section
.agents/skills/<domain>-guardrails/SKILL.mdStandalone 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:

FileGuardrail DomainScope
CLAUDE.mdAll domains (summary)Root — always loaded
backend/CLAUDE.mdBackend architectureLoaded when working in backend/
frontend/CLAUDE.mdFrontend architectureLoaded when working in frontend/
.claude/skills/docs/billing/SKILL.mdBilling (Claude Code)Loaded via /docs/billing skill
.claude/skills/docs/multi-tenancy/SKILL.mdTenancy (Claude Code)Loaded via /docs/multi-tenancy skill
.claude/skills/api-contracts/SKILL.mdAPI format (Claude Code)Loaded via /api-contracts skill
.agents/skills/docs/billing-guardrails/SKILL.mdBilling (Codex)Loaded via Codex skill system
docs/billing/BILLING.mdBilling architectureSource of truth — referenced by skills
docs/billing/CURRENCY-RULES.mdCurrency invariantsSource of truth — 5 non-negotiable rules
docs/billing/LIMITATIONS.mdV1 limitationsSource of truth — intentional constraints
docs/api/CONTRACTS.mdAPI contractsSource of truth — response format rules
Guardrails follow a hierarchy: 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