How to build a SaaS with Claude.
A working blueprint for an AI-native SaaS on Claude — the Messages API at the core, typed tool use for the work, and the multi-tenant, billing, and eval scaffolding that keeps it shippable.
Most "AI SaaS" projects stall in the same place: a demo that dazzles in the room and falls over the moment a second tenant, a real invoice, or an unhappy-path prompt shows up. The fix is not a bigger prompt — it is treating the model as one well-bounded service inside an otherwise ordinary, well-engineered application. This is how we build a SaaS with Claude when we intend to operate it afterwards.
Why Claude is a good spine for a SaaS
A SaaS backend mostly does the same four things over and over: take a request, decide what to do, do it against your own systems, and return something a UI or another service can trust. Claude maps cleanly onto that loop. The Messages API gives you a stateless request/response surface you can put behind your own auth and rate limits. Tool use lets the model call your functions instead of hallucinating their results. Structured outputs let you pin the shape of what comes back so your TypeScript types stay honest. And the current 1M-token context window means you can hand the model a genuinely large working set — a full ticket history, a contract, a code module — without shredding it into lossy chunks.
- Messages API — the stateless core call; you own sessions, auth, and persistence around it.
- Tool use — the model proposes a typed function call; your code executes it and returns the result.
- Structured outputs — responses constrained to a schema you define, so parsing is deterministic.
- 1M-token context window — large working sets stay intact instead of being chunked into a lossy retrieval pipeline.
- Claude Agent SDK and Model Context Protocol (MCP) — when the loop needs to be agentic or to reach shared tools, not bespoke glue.
We build on Claude by default — Claude-native, not a thin wrapper over a model picker. That focus is the point: one model family (Claude Opus 4.8, Claude Sonnet 4.6, Claude Haiku 4.5, and Claude Fable 5), one set of build surfaces, deep familiarity with how each behaves under load. If you need the model close to your data, Claude is also available on AWS Bedrock and Google Vertex AI, which matters for the residency conversation later.
A reference architecture you can actually deploy
Keep the model behind a service boundary. Your API layer handles auth, tenancy, and rate limits; a thin "reasoning service" owns every call to Claude; your existing databases and integrations sit behind tools the model is allowed to call. The model never touches your database directly — it asks for a tool, your code decides whether that tenant is allowed to run it, and only then does the work happen.
- Edge / API gateway — authenticates the user, resolves the tenant, applies quotas.
- Reasoning service — the only place that holds the Claude API key and constructs Messages API calls.
- Tool layer — typed functions (read invoice, search docs, create ticket) the model may invoke; each enforces tenant scope.
- Data + integrations — your Postgres, your queue, your third-party APIs, untouched by the model directly.
- Eval + telemetry sink — every call, tool invocation, and outcome is recorded for the feedback loop in section 05.
The single most valuable habit is defining tools with a strict input schema and validating the model’s arguments before you execute anything. The tool definition is a contract: the model fills it in, your code checks it, the work runs. Here is the shape we reach for first.
import Anthropic from '@anthropic-ai/sdk'import { z } from 'zod' const client = new Anthropic() // 1. The contract the model fills in.const createTicketArgs = z.object({ subject: z.string().min(3).max(120), priority: z.enum(['low', 'normal', 'high']), body: z.string().min(1).max(4000),}) const tools = [ { name: 'create_ticket', description: 'Open a support ticket for the current tenant.', input_schema: { type: 'object', properties: { subject: { type: 'string' }, priority: { type: 'string', enum: ['low', 'normal', 'high'] }, body: { type: 'string' }, }, required: ['subject', 'priority', 'body'], }, },] as const async function handleTurn(tenantId: string, prompt: string) { const res = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools, messages: [{ role: 'user', content: prompt }], }) for (const block of res.content) { if (block.type !== 'tool_use') continue // 2. Never trust the args — parse them. const args = createTicketArgs.parse(block.input) // 3. Tenant scope is enforced here, not in the prompt. await tickets.create({ tenantId, ...args }) } return res}Tenant isolation, quotas, and permissions live in code, around the tool call — never in instructions you hope the model obeys. Treat every tool argument as untrusted input and validate it before it touches your data.
Multi-tenancy, billing, and auth
This is where AI SaaS projects quietly become normal SaaS projects. The model does not change the fundamentals: every row needs a tenant id, every query needs to be scoped, and a noisy tenant must not be able to drain a shared budget. What the model adds is a new, expensive, variable-cost resource — tokens — that you now have to meter and bill like any other unit of consumption.
- Isolation — scope every tool and query by tenant id; prefer row-level security so a missing filter fails closed.
- Metering — record input and output tokens per request, attributed to a tenant, so usage is auditable and billable.
- Billing — meter the underlying token consumption, then price it as your own product tiers; do not pass raw model costs through verbatim, and expect model economics to drift over time.
- Quotas — per-tenant rate and spend caps so one account cannot exhaust a shared budget or starve others.
- Auth — the user’s session resolves the tenant before any model call; the reasoning service never infers tenancy from prompt text.
On cost: tier deliberately. Route the bulk of high-volume, low-stakes traffic to a smaller, faster model and reserve the largest model for the turns that genuinely need its reasoning. That single routing decision usually moves your unit economics more than any prompt optimisation, and it is far more stable than chasing whatever the current per-token pricing happens to be.
Make outputs typed, not hopeful
A SaaS API cannot return free-form prose to a frontend that expects a record. Constrain the model’s output to a schema and validate it on the way out, exactly as you validated tool arguments on the way in. When the response shape is guaranteed, the rest of your stack — caching, queues, the UI — can treat the reasoning service like any other typed dependency.
- Define the response schema once and share it between the model call and your application types.
- Validate every response; on a schema miss, retry or fail loudly rather than shipping a malformed record downstream.
- Keep the schema small and explicit — narrow enums and required fields are easier for the model to honour and for you to test.
Evals and observability are the product, not an afterthought
You cannot operate what you cannot measure, and "it looked right in the demo" is not a measurement. Before you ship, build a small evaluation set of real inputs with known-good outcomes, and run every prompt or model change against it. In production, log every model call, every tool invocation, latency, token counts, and the eventual outcome — so a regression shows up as a number, not a support ticket.
| Layer | What you watch | Why it matters |
|---|---|---|
| Offline evals | Pass rate on a curated test set | Catches regressions before deploy, not after |
| Tool calls | Argument-validation failures, denials | Surfaces prompt drift and isolation bugs early |
| Latency | Per-model response time, p95 | Protects the UX; flags when to re-tier traffic |
| Token usage | Input/output tokens per tenant | Feeds billing and spots runaway prompts |
| Outcomes | Human or downstream acceptance | The only metric that maps to real value |
Treat the eval set as living code. Every escaped defect becomes a new test case, so the same failure cannot ship twice. This is the discipline that turns a clever prototype into something you can put an SLA on.
The model is the easy part. The hard part — tenancy, metering, evals, the unhappy paths — is ordinary software engineering, and that is precisely why it ships.
Ship in cycles, then operate it
We do not build a SaaS with Claude as a one-off handoff. We ship in two-week cycles against a working blueprint, then operate what we shipped — on-call, SLAs, and an eval loop that keeps the model honest as your usage grows. If you want the whole build owned end to end, that is exactly what Custom SaaS is. When the leverage is in automating an internal workflow rather than a full product, AI workflow automation is the narrower path, and when the model needs to reach shared tools cleanly, MCP server development is how we wire it.
Bring a discovery call — show us the workflow you want as software. We read code on the call, scope the first cycle, and tell you honestly whether Claude is the right spine. See how engagements are priced.
Questions, answered.
Start with single Messages API calls and typed tool use — most SaaS features need nothing more. Reach for the Claude Agent SDK or a multi-step loop only when a task genuinely requires the model to plan across several tool calls. Adding agent machinery before you need it is the most common over-engineering trap.
Never rely on the prompt for isolation. Resolve the tenant from the authenticated session before any model call, scope every tool and query by tenant id (row-level security is ideal), and validate tool arguments in code before executing them. The prompt is an instruction, not a security boundary.
Meter the underlying token consumption per tenant and price it as your own product tiers rather than passing raw model costs through. Tier your model routing — smaller models for high-volume work, the largest only when reasoning demands it — so your unit economics stay stable even as model pricing drifts.
Match the model to the stakes. Route high-volume, low-stakes turns to a faster model like Claude Haiku 4.5, use Claude Sonnet 4.6 for the general workload, and reserve Claude Opus 4.8 for the turns that truly need its reasoning. Make this a routing decision in code, not a single global default.
Build a small evaluation set of real inputs with known-good outcomes and run every prompt or model change against it before deploy. In production, log calls, tool invocations, latency, tokens, and outcomes, and promote every escaped defect into a new eval case so the same failure cannot ship twice.
Claude is available on AWS Bedrock and Google Vertex AI in addition to the Anthropic Messages API, so you can keep inference inside your chosen cloud and region. That flexibility is what makes residency-sensitive builds — including sovereign deployments — practical.