Skip to content
SaaS4Builders
Architecture for AI Agents

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.

Six convention documents form the project's living specification — over 3,900 lines of structured patterns, rules, and templates. They serve double duty: they document the project for human developers AND constrain AI agent output so it matches the architecture.

When an AI agent needs to create a new API endpoint, it reads the API Contracts document for the response format, the Backend Templates for the code structure, and the Query Builder Guide for filter/sort conventions. The output follows the same patterns as every other endpoint in the project — because the agent read the same rules.

This page covers what each document contains and how to extend them for new domains.


The Convention Document Inventory

DocumentLocationLinesPrimary Audience
API Contractsdocs/api/CONTRACTS.md~990Backend + Frontend
Backend Templatesdocs/architecture/backend/TEMPLATES.md~1,130Backend
Frontend Templatesdocs/architecture/frontend/TEMPLATES.md~910Frontend
Query Builder Guidedocs/architecture/backend/QUERY-BUILDER.md~600Backend
Domain Glossaryai-context/DOMAIN-GLOSSARY.md~110All
Decision Log.claude/decisions.md~170All

Together, these documents answer every question an AI agent might have about how code should be structured, named, and validated in this project.


API Contracts (~990 lines)

The largest and most cross-cutting convention document. It governs every HTTP interaction between backend and frontend — the format of every request, response, error, and pagination envelope.

Global Response Conventions

Casing — All JSON keys in API requests and responses are snake_case. The frontend converts snake_case to camelCase on receive and reverses the transformation on send. This is handled by the API client layer automatically.

Backend sends:  created_at, updated_at, tenant_id, plan_id
Frontend uses:  createdAt, updatedAt, tenantId, planId

Dates — Date-only values use YYYY-MM-DD. Datetime values use ISO-8601 UTC:

2025-12-26
2025-12-26T14:35:21Z

IDs — UUIDs for external/public identifiers. Integer IDs are internal and not exposed unless explicitly intended.

Money — Integer minor units plus currency code. Never floats:

{ "amount_cents": 1299, "currency": "EUR" }

This means $12.99 EUR. The integer representation eliminates floating-point rounding errors in billing calculations.

Booleans and enums — Booleans are strict JSON booleans. Enums are stable lowercase string values ("active", "trialing", "canceled"). Never localized labels.

Pagination Contract

Non-paginated list endpoints always wrap results in a data key:

{ "data": [] }

Paginated endpoints add a meta object:

{
  "data": [],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 25,
    "total": 0
  }
}

Query parameters: page (integer >= 1), per_page (default 25, max 200). The backend enforces the maximum.

Error Format

All errors return JSON with a consistent structure:

Validation errors (422):

{
  "message": "Validation failed",
  "errors": {
    "field": ["Error message"]
  }
}

Domain error codes:

{
  "message": "Plan upgrade not allowed",
  "code": "PLAN_UPGRADE_NOT_ALLOWED"
}

The code field is stable and machine-readable — frontend code can switch on it. The message field is human-readable and may change.

Standard HTTP errors follow the same pattern: 401 Unauthenticated, 403 Forbidden, 404 Not Found, 419 CSRF Token Mismatch, 429 Rate Limited.

Frontend Synchronization (Resource to Schema)

The API Contracts document defines a naming convention that maps backend Resources to frontend Zod schemas:

{ResourceName} → {resourceName}Schema

Remove the Resource suffix, convert to camelCase, add Schema. For example:

  • SubscriptionResource becomes subscriptionSchema
  • InvoiceResource becomes invoiceSchema
  • TenantUserResource becomes tenantUserSchema

The document includes a complete Resource-to-Schema mapping table. Here is a representative sample:

Foundation:

Backend ResourceFrontend SchemaLocation
SessionResourcesessionSchemafeatures/foundation/auth/schemas.ts
TokenResourcetokenSchemafeatures/foundation/auth/schemas.ts
TenantResourcetenantSchemafeatures/foundation/tenancy/schemas.ts
EntitlementsResourceentitlementsSchemafeatures/foundation/entitlements/schemas.ts

Core — Billing:

Backend ResourceFrontend SchemaLocation
SubscriptionResourcesubscriptionSchemafeatures/core/docs/billing/schemas.ts
InvoiceResourceinvoiceSchemafeatures/core/docs/billing/schemas.ts
PaymentMethodResourcepaymentMethodSchemafeatures/core/docs/billing/schemas.ts
UsageReportResourceusageReportSchemafeatures/core/docs/billing/schemas.ts

Core — Team:

Backend ResourceFrontend SchemaLocation
TenantUserResourcetenantUserSchemafeatures/core/team/schemas.ts
InvitationResourceinvitationSchemafeatures/core/team/schemas.ts
RoleResourceroleSchemafeatures/core/team/schemas.ts

The full table covers Foundation, Core (Catalog, Billing, Team), and Admin resources — every Resource in the project has a corresponding Schema entry.

The Resource-to-Schema mapping table is the single source of truth for backend-frontend type synchronization. When you add a new Resource, add its corresponding Schema entry to this table. AI agents use it to generate matching Zod schemas automatically.

Backend Templates (~1,130 lines)

Code templates for every backend pattern. An AI agent copies these templates and adapts them to the specific domain, ensuring consistent file structure, naming, and conventions.

What the Templates Cover

TemplateFiles Generated
Query (List/Search)Request, Filters DTO, Query class, Resource
Action (Create/Update)Request, Data DTO, Action class, Resource
External IntegrationContract interface, Provider implementation, Mapper, Domain DTO
TestsFeature test scaffold, Unit test scaffold, Tenant isolation test

Additionally, the document covers Resource patterns (money fields, nested relationships, enum fields, computed fields) and a Contract Compliance Checklist.

Query Template Example

The Query template defines the standard flow for read operations:

Files to create:

Application/<Domain>/Queries/ListX.php
Application/<Domain>/DTO/ListXFilters.php
Http/Requests/Api/V1/<Context>/ListXRequest.php
Http/Resources/Api/V1/<Context>/XResource.php

The Request with toDto():

Http/Requests/Api/V1/Tenant/ListXRequest.php
final class ListXRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'q'        => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
        ];
    }

    public function toDto(): ListXFilters
    {
        return new ListXFilters(
            q: $this->validated('q'),
            perPage: (int) ($this->validated('per_page') ?? 50),
        );
    }
}

The DTO:

Application/<Domain>/DTO/ListXFilters.php
final readonly class ListXFilters
{
    public function __construct(
        public ?string $q = null,
        public int $perPage = 50,
    ) {}
}

Every query follows this exact pattern: FormRequest validates and maps to a readonly DTO, the Query class executes the read, and the Resource formats the output in snake_case.

Action Template Example

The Action template defines the standard flow for mutations:

Request → toDto() → CreateXData DTO → CreateX Action → XResource

Actions wrap their logic in DB::transaction(), return an Eloquent Model, and never call external services directly — they depend on Domain Contracts (interfaces).

How Templates Keep Output Consistent

  • Every new feature follows the same file structure and naming conventions
  • The agent matches the template patterns: final classes, readonly DTOs, toDto() in Requests
  • Resources always output snake_case keys with proper money and date formatting
  • Test templates include tenant isolation scenarios by default, so isolation testing is never forgotten

Frontend Templates (~910 lines)

What the Templates Cover

TemplateFiles Generated
Feature ScaffoldDirectory + required files (index.ts, types.ts, schemas.ts)
schemas.tsZod response schemas, input schemas, type exports
types.tsTypeScript types inferred from schemas
API ModuleFeature API with full CRUD, Zod validation on every response
ComposableReactive wrapper with useAuthenticatedAsyncData
index.tsBarrel exports — the feature's public API
ComponentVue component consuming a composable
Store (rare)Pinia store for cross-feature global state
Foundation FeatureSimplified pattern without API folder

Feature Scaffold Example

Every new feature starts with a scaffold command:

# Create a new core feature
FEATURE=notifications
mkdir -p features/core/$FEATURE/{composables,components,api}
touch features/core/$FEATURE/{index.ts,types.ts,schemas.ts}
touch features/core/$FEATURE/api/$FEATURE.api.ts
touch features/core/$FEATURE/composables/use${FEATURE^}.ts

The resulting structure:

features/core/docs/notifications/
├── index.ts       # Public API (barrel exports) — REQUIRED
├── types.ts       # TypeScript types — REQUIRED
├── schemas.ts     # Zod schemas — REQUIRED
├── api/
│   └── notifications.api.ts
├── composables/
│   └── useNotifications.ts
└── components/

Schema Template Example

All schemas follow the conventions defined in the API Contracts document:

features/core/<feature>/schemas.ts
import { z } from 'zod'
import { moneySchema } from '@common/types/money'

// --- Response Schemas ---

export const notificationSchema = z.object({
  id: z.string().uuid(),
  type: z.enum(['email', 'push', 'sms']),
  channel: z.enum(['immediate', 'batched']),
  // Use z.string().datetime() for dates (ISO-8601)
  // Use moneySchema for money fields
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
})

export const notificationListSchema = z.array(notificationSchema)

Key conventions: z.string().uuid() for IDs, z.string().datetime() for ISO-8601 dates, z.enum([...]) for status and type fields, moneySchema for money fields. All schemas use camelCase because the API client transforms snake_case before Zod parsing.

How Templates Keep Output Consistent

  • Every feature has the same directory structure: index.ts, types.ts, schemas.ts are always present
  • index.ts exposes only the public API — internal implementation is never imported directly
  • API modules always validate responses with Zod .parse(), catching contract drift at runtime
  • Composables use useAuthenticatedAsyncData with namespaced keys (e.g., billing:subscription)

Query Builder Guide (~600 lines)

The Query Builder Guide documents Spatie QueryBuilder conventions for list endpoints with dynamic filtering, sorting, and relationship includes.

When to Use QueryBuilder

Use CaseUse QueryBuilder?
Admin list with dynamic filtersYes
Paginated list with user sortingYes
Multi-criteria searchYes
Simple endpoint without filteringNo — use classic Eloquent
Single resource detail (show)No
Static catalog (public plans)No

General rule: Use QueryBuilder whenever an endpoint exposes client-side filters or sorting.

Architecture Rule

QueryBuilder is used exclusively in Queries (Application/*/Queries/), never in Actions or Controllers.

Application/
└── Tenant/
    ├── Queries/
    │   ├── ListTenants.php          # Uses QueryBuilder
    │   └── FindTenantById.php       # Simple Eloquent (no QueryBuilder)
    └── DTO/
        └── ListTenantsFilters.php   # Filter DTO

This keeps the filtering logic contained. Controllers delegate to Queries, and Queries own the query-building logic.

What the Guide Covers

  • Filter types — Exact match, partial (LIKE), scope-based, custom callbacks, relationship filters
  • Sort conventionssort=field ascending, sort=-field descending. Only allow sorting on an explicit allowlist per endpoint.
  • Includes — Allowlisted relationships that can be loaded via ?include=plans,features. No unbounded relationship loading.
  • SecurityWhitelist everything. Filters, sorts, and includes must be explicitly declared with allowedFilters(), allowedSorts(), and allowedIncludes(). Undeclared parameters are rejected.
  • Frontend integration — How filter and sort parameters are sent from the Nuxt frontend, including the buildQueryParams() helper function
  • Testing — Filter tests, sort tests, invalid filter handling, relationship include tests
The security whitelist is non-negotiable. Every QueryBuilder endpoint must use allowedFilters(), allowedSorts(), and allowedIncludes() to explicitly declare what the client can request. Without this, the client could sort on sensitive fields or load unintended relationships.

Domain Glossary (~110 lines)

The Domain Glossary defines canonical terminology for the project — the ubiquitous language that both developers and AI agents use when writing code and documentation.

What It Defines

Core SaaS Concepts:

TermDefinition
TenantA customer organization (or account) using the SaaS. All resources are scoped by tenant.
SeatBillable user slot under a tenant.
UserAn individual authenticated account belonging to one or more tenants.
ProductA logical unit of value offered (e.g., "Sessana Pro", "Analytics Suite").
PlanA pricing configuration for a product (monthly/yearly, limits, features).
SubscriptionBinding between a tenant and a plan (plus add-ons, billing cycle, status).

Entitlements and Usage:

TermDefinition
EntitlementA rule or object defining access to a specific feature or capability.
FeatureA functional capability of the SaaS (e.g., "Custom domains", "Team members").
QuotaA numeric limit (e.g., "10 projects", "5 team members").
MeterA counter tracking a specific type of usage (e.g., "API calls", "sent emails").
Usage EventA recorded event contributing to one or more meters.

Architecture and Operations:

Terms like CI/CD, Octane (high-performance Laravel runtime), and the project's internal context system.

How It Keeps Output Consistent

When an AI agent writes code, it uses the exact terms defined in the glossary. The entity is always "Tenant", never "Organization" or "Account". The relationship is always "Subscription", never "License" or "Membership". This prevents synonym drift — a subtle but pervasive problem when multiple developers and AI agents work on the same codebase.

When you add a new business domain to the project, add its core terms to the Domain Glossary. AI agents will use the correct terminology from the first prompt instead of guessing or using synonyms.

How Convention Documents Work Together

When a task requires multiple conventions, the agent reads several documents and applies them simultaneously:

Task: "Add a filterable list endpoint for notifications"

Agent reads:
├── API Contracts     → response format, pagination, error shapes
├── Backend Templates → Query template, Request template, Resource template
├── Query Builder     → filter/sort conventions, security whitelist
└── Domain Glossary   → "Notification" definition and relationships

Output:
├── ListNotificationsRequest.php     (rules from Templates, pagination from Contracts)
├── ListNotificationsFilters.php     (readonly DTO from Templates)
├── ListNotifications.php            (QueryBuilder from QB Guide, in Queries/ from Templates)
├── NotificationResource.php         (snake_case from Contracts, money format from Contracts)
└── ListNotificationsTest.php        (filter tests from QB Guide, isolation from Templates)

The output follows four convention sets simultaneously — because the agent read all four documents before writing code. This is what makes the convention system powerful: each document addresses a different concern, and together they produce code that matches the architecture on every dimension.


Extending for New Domains

When you add a new feature to the project, keep the convention documents up to date so AI agents produce consistent output in your new domain.

Adding Entities to the Glossary

Add the term and definition to the appropriate section of ai-context/DOMAIN-GLOSSARY.md. Keep definitions to one sentence:

**Notification**
A message sent to a user via push, email, or SMS — tracked with delivery status.

Adding Backend Templates

If a new code pattern emerges that does not fit the existing templates (e.g., event sourcing, background jobs), add it as a new numbered section in docs/architecture/backend/TEMPLATES.md. Follow the existing format: files to create, template code, flow diagram.

Adding Frontend Templates

Add new component patterns or feature types as new sections in docs/architecture/frontend/TEMPLATES.md. Reference existing templates to show the standard structure.

Keeping Documents in Sync

Convention documents are living documents — update them when patterns change. Two mechanisms help catch drift:

  • The docs-sync skill (Codex) checks whether a change requires updating a canonical document and identifies which one
  • The /review-code skill (Claude Code) includes a contract compliance check in its review checklist

The general rule: if code and docs disagree, fix the doc or call out the mismatch explicitly. Never let the drift accumulate silently — it degrades AI output quality over time because agents trust the documents.


What's Next