SaaS Starter

AI-ready

How this boilerplate is optimized so an AI agent can write code that actually works.

Last updated May 3, 2026

Philosophy

LLMs don't read your mind — they read your code. An AI-friendly boilerplate minimizes ambiguity: exact types, repeated patterns, explicit ADRs.

Types as contract

@app/contracts exports Zod schemas and inferred types. An agent that reads the contract knows exactly which payload to send and what to expect back. No documentation to rot.

import { CreateUserInput } from '@app/contracts';
// The type is the doc.

Branded IDs

UserId ≠ OrganizationId ≠ string. If an agent confuses one for another, the TS server catches it before the commit. Fewer subtle bugs.

Result instead of throws

LLMs propagate errors better when they are values. Result<T, E> maps naturally to JSON, to tests, to docs.

Versioned ADRs

docs/adr/ documents architectural decisions (why BetterAuth, why provider-agnostic billing). An agent reads the ADR before proposing a change that contradicts it.

Generators

bun run gen:domain <name> scaffolds a complete aggregate (entity, VOs, repo, use cases, controller, tests). The agent follows the existing pattern instead of inventing a new one.

MCP-friendly

Prisma schemas + OpenAPI generated at apps/server/openapi.json. Any agent with OpenAPI tools explores the API without guesswork.

Repeated patterns

Every aggregate has the same structure (domain/, application/, infrastructure/, interface/). An LLM learns the pattern in one bounded context and replicates it in the next without friction.

Chat endpoint guardrails

POST /api/v1/ai/chat is metered by real money, so it is locked down:

  • Clients may only send user and assistant messages. A system role is rejected by the @app/contracts schema.
  • model must be AI_MODEL or listed in AI_ALLOWED_MODELS; anything else is a 422 before any upstream call.
  • Output is capped by AI_MAX_OUTPUT_TOKENS.
  • When the client disconnects, the upstream stream is aborted so a closed tab stops spending tokens.

[!NOTE] Under Bun 1.3.x a POST whose body was already consumed does not emit a disconnect event, so the disconnect abort may not fire there. Two hard bounds cover it: AI_MAX_OUTPUT_TOKENS and AI_STREAM_TIMEOUT_MS (default 120000), a wall-clock cap that aborts the upstream call. The stream is also aborted if a write to the response throws or the response is destroyed.

On this page