Convention Files
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
| Document | Location | Lines | Primary Audience |
|---|---|---|---|
| API Contracts | docs/api/CONTRACTS.md | ~990 | Backend + Frontend |
| Backend Templates | docs/architecture/backend/TEMPLATES.md | ~1,130 | Backend |
| Frontend Templates | docs/architecture/frontend/TEMPLATES.md | ~910 | Frontend |
| Query Builder Guide | docs/architecture/backend/QUERY-BUILDER.md | ~600 | Backend |
| Domain Glossary | ai-context/DOMAIN-GLOSSARY.md | ~110 | All |
| Decision Log | .claude/decisions.md | ~170 | All |
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:
SubscriptionResourcebecomessubscriptionSchemaInvoiceResourcebecomesinvoiceSchemaTenantUserResourcebecomestenantUserSchema
The document includes a complete Resource-to-Schema mapping table. Here is a representative sample:
Foundation:
| Backend Resource | Frontend Schema | Location |
|---|---|---|
SessionResource | sessionSchema | features/foundation/auth/schemas.ts |
TokenResource | tokenSchema | features/foundation/auth/schemas.ts |
TenantResource | tenantSchema | features/foundation/tenancy/schemas.ts |
EntitlementsResource | entitlementsSchema | features/foundation/entitlements/schemas.ts |
Core — Billing:
| Backend Resource | Frontend Schema | Location |
|---|---|---|
SubscriptionResource | subscriptionSchema | features/core/docs/billing/schemas.ts |
InvoiceResource | invoiceSchema | features/core/docs/billing/schemas.ts |
PaymentMethodResource | paymentMethodSchema | features/core/docs/billing/schemas.ts |
UsageReportResource | usageReportSchema | features/core/docs/billing/schemas.ts |
Core — Team:
| Backend Resource | Frontend Schema | Location |
|---|---|---|
TenantUserResource | tenantUserSchema | features/core/team/schemas.ts |
InvitationResource | invitationSchema | features/core/team/schemas.ts |
RoleResource | roleSchema | features/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.
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
| Template | Files Generated |
|---|---|
| Query (List/Search) | Request, Filters DTO, Query class, Resource |
| Action (Create/Update) | Request, Data DTO, Action class, Resource |
| External Integration | Contract interface, Provider implementation, Mapper, Domain DTO |
| Tests | Feature 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():
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:
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:
finalclasses,readonlyDTOs,toDto()in Requests - Resources always output
snake_casekeys 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
| Template | Files Generated |
|---|---|
| Feature Scaffold | Directory + required files (index.ts, types.ts, schemas.ts) |
| schemas.ts | Zod response schemas, input schemas, type exports |
| types.ts | TypeScript types inferred from schemas |
| API Module | Feature API with full CRUD, Zod validation on every response |
| Composable | Reactive wrapper with useAuthenticatedAsyncData |
| index.ts | Barrel exports — the feature's public API |
| Component | Vue component consuming a composable |
| Store (rare) | Pinia store for cross-feature global state |
| Foundation Feature | Simplified 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:
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.tsare always present index.tsexposes 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
useAuthenticatedAsyncDatawith 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 Case | Use QueryBuilder? |
|---|---|
| Admin list with dynamic filters | Yes |
| Paginated list with user sorting | Yes |
| Multi-criteria search | Yes |
| Simple endpoint without filtering | No — 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 conventions —
sort=fieldascending,sort=-fielddescending. Only allow sorting on an explicit allowlist per endpoint. - Includes — Allowlisted relationships that can be loaded via
?include=plans,features. No unbounded relationship loading. - Security — Whitelist everything. Filters, sorts, and includes must be explicitly declared with
allowedFilters(),allowedSorts(), andallowedIncludes(). 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
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:
| Term | Definition |
|---|---|
| Tenant | A customer organization (or account) using the SaaS. All resources are scoped by tenant. |
| Seat | Billable user slot under a tenant. |
| User | An individual authenticated account belonging to one or more tenants. |
| Product | A logical unit of value offered (e.g., "Sessana Pro", "Analytics Suite"). |
| Plan | A pricing configuration for a product (monthly/yearly, limits, features). |
| Subscription | Binding between a tenant and a plan (plus add-ons, billing cycle, status). |
Entitlements and Usage:
| Term | Definition |
|---|---|
| Entitlement | A rule or object defining access to a specific feature or capability. |
| Feature | A functional capability of the SaaS (e.g., "Custom domains", "Team members"). |
| Quota | A numeric limit (e.g., "10 projects", "5 team members"). |
| Meter | A counter tracking a specific type of usage (e.g., "API calls", "sent emails"). |
| Usage Event | A 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.
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-syncskill (Codex) checks whether a change requires updating a canonical document and identifies which one - The
/review-codeskill (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
- AI-Assisted Development Overview — The three pillars: configuration, conventions, execution
- CLAUDE.md Configuration — How Claude Code loads these conventions
- Setup and Configuration — How Codex references these conventions via AGENTS.md
- Architecture Overview — The project architecture these conventions describe
- API Contracts — Detailed documentation of the API contract system
Adapting Conventions
How to translate the boilerplate's AI conventions to Codex, Cursor, Copilot, or any AI agent: context mapping, skill adaptation, execution templates, and quality gates.
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.