[ Blog ]
Building AI SaaS, in detail
Production notes from building Shipwright — the parts of a Claude-powered app that boilerplates skip. Every article starts from a real bill, a real outage, or a real production decision, and ends with the code that fixed it.
- — Streaming that survives client disconnects
- — Per-request token and cost metering
- — Prompt versioning and evals in CI
- — Quotas, model gating, and Stripe billing
Everything described here runs live in the demo chat, which shows the exact cost of every request as you use it — the same code these posts take apart.
A data backfill as a Drizzle custom migration: the ON CONFLICT that fails it and the query that finds what it skipped
Backfill data with a Drizzle custom migration, tested: why ON CONFLICT with a target fails, why a rerun is safe, and the query that finds skipped rows.
Read →A Postgres rate limiter for serverless Next.js, without Redis: one upsert, tested across four instances
A Postgres rate limiter for serverless Next.js without Redis: one atomic upsert, tested across four processes. In-memory let 80 through, Postgres 20.
Read →Postgres composite index column order: why the usage index is (user_id, created_at), measured on 2 million rows
Postgres composite index column order, measured on 2M usage rows: about 1 ms in the right order, 10 to 16 ms reversed, plus a covering index trap.
Read →react-markdown as a Next.js server component: 34 KB less JavaScript, and what the page pays instead
Moving react-markdown to a Next.js server component, measured on one post: 34 KB less gzipped script, a bigger RSC payload, and when to keep it.
Read →Postgres does not index foreign keys for you: the Drizzle index this schema is missing, measured
Postgres does not index foreign key columns. Measured on 200k users: an 18 ms lookup per request becomes 0.1 ms, plus the Drizzle migration trap.
Read →Drizzle timestamp vs withTimezone in Postgres: the hour that appears when the server is not on UTC
Drizzle timestamp vs withTimezone, tested on a Postgres set to UTC+1: what round-trips, the default that reads an hour ahead, and the migration trap.
Read →x-forwarded-for spoofing and IP rate limits in Next.js: the header is only as honest as the proxy in front of it
IP rate limits in Next.js trust x-forwarded-for. Tested: Vercel overwrites it, an appending proxy leaves the client in charge of the first entry. The fix.
Read →SEO titles and descriptions in Next.js generateMetadata: measure pixels, keep a second title, skip the quotes
SEO title and description length in Next.js generateMetadata: measure pixels, not characters, keep a short metaTitle per post, and avoid entities.
Read →Stripe subscription status and paid access: what past_due, unpaid and incomplete should mean in your app
Stripe subscription status decides paid access: what past_due, unpaid and incomplete mean, why checkout needs a second list, and a grace period.
Read →A SaaS pricing page built from the plan config, and the two values that still drift
Build the pricing page from the plan config that enforces quotas and model access, then guard the two values that still drift: feature copy and Stripe.
Read →Next.js not-found pages: keep the real 404, and the loading.tsx that turns it into a soft 404
A custom not-found page in Next.js answers with a real 404 until a loading.tsx starts streaming first. Tested on Next.js 16: what returns 200 and why.
Read →Shipping a Drizzle schema change when the deploy does not run migrations
When the deploy does not run migrations, order matters: apply the additive Drizzle migration first, then ship the code that uses it. A real rollout.
Read →Rendering a streaming LLM response in React: the state updates that do not lose text
Streaming model tokens into React without dropping text: a placeholder bubble, functional updates per delta, batched renders, one cleanup for errors.
Read →Open Graph images in the Next.js App Router: generate the card in code, and do not lose it in a page
Share cards in Next.js: generate them with ImageResponse, stay inside the renderer limits, and avoid the metadata merge that silently drops the image.
Read →Node.js vs Edge runtime for AI routes in Next.js: why every model and billing route stays on Node
AI and billing routes in Next.js 16 belong on the Node.js runtime: database drivers need TCP, streaming works on both, and edge fails at request time.
Read →Next.js 16 renamed middleware to proxy.ts: what actually changed, and what to stop doing there
Next.js 16 renamed middleware to proxy.ts and made Node.js its default runtime. The codemod, the matcher traps, and the jobs proxy should not take on.
Read →Generate your sitemap and llms.txt from one source in Next.js, so neither goes stale
One posts array feeds the blog, sitemap.xml, robots.txt, and llms.txt, so publishing a post updates every discovery file and none of them can go stale.
Read →Postgres connection pooling for serverless Next.js: why connections multiply, and how to stop it
Each serverless instance opens its own Postgres pool, so connections multiply with traffic: size the pool, add a pooler, and know when prepare must go.
Read →Validate your LLM request body with Zod: the cheapest cost control you are not counting
A Zod schema on your chat route is a cost control. Cap message size and history so one request cannot balloon the bill, and add the total cap people miss.
Read →Stripe Checkout and the customer portal in the Next.js App Router: two routes, one writer
Wiring Stripe Checkout and the customer portal in the Next.js App Router: reuse one customer, set return URLs, and let the webhook be the only writer.
Read →Show users their AI usage: a Next.js dashboard that warns before the 402
A server-rendered usage page that shows requests against the plan cap, tokens, and sub-cent costs, so users see the limit before a 402 tells them.
Read →The Claude thinking parameter: why Haiku 4.5 returns a 400, and how to gate it per model
Adaptive thinking runs on Opus 4.8 and Sonnet 5, Haiku 4.5 rejects it with a 400, and omitting it turns thinking off on Opus. Gate it in one helper.
Read →Stripe webhook signature verification in a Next.js route handler, done right
Verifying Stripe webhooks in a Next.js route: read the raw body, use constructEventAsync, respect the replay window, and reply so Stripe can retry.
Read →Claude's 1M-token context window: when it is worth the cost, and when retrieval wins
A 1M-token prompt costs about $5 of input per request on Opus, resent every turn. When long context beats retrieval, and how caching changes the math.
Read →Next.js build failing on missing env vars? Make your Stripe and database clients lazy
Module-level Stripe and database clients break next build without secrets. Lazy singletons, presence guards, and deciding fail-open vs fail-closed.
Read →Handling Claude API 429s in Next.js: what the SDK retries, and what reaches your users
What the SDK already retries on a 429, what it cannot retry once tokens are flowing, and how to show users a clean message instead of a stack trace.
Read →Typed tool use with Claude and Zod: the agent loop, and the usage you forget to bill
One Zod schema per tool gives the API schema, runtime checks, and types. Plus the agent loop detail billing needs: usage summed across every iteration.
Read →SSE vs WebSockets for LLM streaming: why one-way token streams want SSE
For one-way token streams, server-sent events beat sockets: plain HTTP, proxy-friendly, and serverless-safe. When sockets win, and the catch.
Read →Versioned prompts in TypeScript: one typed registry instead of scattered strings
Prompts scattered across routes make quality drops a mystery. A typed, versioned registry makes every prompt edit reviewable, testable and cache-safe.
Read →Model gating by subscription plan: make it one enum, not scattered if-else
Routing free users to Haiku and paid to Opus should be one enum and one registry lookup, not if-else scattered across routes. The plan-gating pattern.
Read →Better Auth on Next.js 16: the three setup gotchas that bite
Better Auth wires up fast, but Next.js 16 adds traps: an optimistic-only proxy check, the camelCase tables it owns, and plugin order that must be right.
Read →LLM-as-judge evals: writing the rubric, and the failure modes that lie to you
Deterministic graders cannot tell if an answer is good. An LLM judge can, with the right rubric and a plan for its failure modes: leniency, drift, and bias.
Read →Rate limiting LLM API routes in Next.js: the limiter, the key, and the Redis seam
A fixed-window limiter keyed by user or IP, wired before the model call to return 429, with a Redis seam for serverless, and what it actually bounds.
Read →How much the Claude API really costs per user: a worked example
A worked example: price one real chat session on Opus, Sonnet, and Haiku at current list prices, then per active user per month, and why it sets tiers.
Read →Why your Next.js SSE stream arrives as one blob (and the settings that fix it)
Your tokens stream server-side but reach the browser all at once. The headers, proxy buffering, and timeouts that make SSE actually stream in Next.js.
Read →Typed output from Claude with Zod: stop parsing prose and delete the retry loop
Stop prompting Claude for JSON and parsing the prose. Constrain the output to a Zod schema, validate it client-side, and delete the retry loop.
Read →Metered billing for an AI SaaS: the 402 pattern and the Stripe gotcha that breaks renewals
How metered AI billing works end to end: return HTTP 402 to drive upgrades, sync subscriptions from Stripe webhooks, and the renewal-date gotcha.
Read →Catch prompt regressions in CI before your users do
Shipping a prompt edit can quietly break quality for your biggest user. An eval harness of deterministic graders plus an LLM judge that fails CI first.
Read →How to keep an AI free tier from bankrupting you: two ceilings and a cheap model
An uncapped free tier on an expensive model is how AI apps lose money. Two ceilings keep it safe: a monthly request quota and per-plan model gating.
Read →Tracking LLM costs per user: the schema that billing can actually trust
Per-user LLM cost tracking that billing can trust: one row per request, integer micro-USD instead of floats, and a monthly rollup that drives quotas.
Read →Claude prompt caching: the break-even math and the mistake that turns it off
Claude cache reads bill at a tenth of the input rate; writes cost 1.25x. The break-even math, the 5-minute TTL, and the mistake that disables caching.
Read →Your Next.js AI streaming route keeps billing you after the user leaves
A streaming LLM route in the Next.js App Router keeps generating tokens after the user closes the tab, and you pay for all of them. Here is the fix.
Read →
New posts land as the kit evolves — when a dependency bumps, a model changes pricing, or a production incident teaches something worth writing down. If you want the full working system instead of piecemeal patterns, the kit is a one-time purchase with twelve months of updates: see pricing for the in-app billing demo, or the landing page for everything that ships inside.