Build It — Your First Claude-Built SaaS
Capstone: ship your AI-backed SaaS, personalized start to finish
The capstone. You've learned five primitives — time to use every one of them on a real project. By the end of this chapter you'll have a live-on-the-internet AI-powered SaaS that you built by driving Claude Code through the habits from the last seven chapters.
This isn't a copy-paste-this-exact-code tutorial. It's a partnered build. You paste the prompts into Claude Code, Claude writes the code, you verify each stage before moving on. If CLAUDE.md, skills, commands, and memory are wired up in your repo from the earlier chapters, you'll move fast.
You'll have shipped your own AI-backed SaaS. Magic-link auth, per-user data isolation via RLS, a Claude-powered feature, deployed on Vercel with a URL you can send to your friends.
What are we building?
Every prompt in this chapter personalizes to your answers. Fill this in once and the rest of the chapter follows your idea — or skip and use the default Notes app.
The pattern, not the product
The Notes app we build here is the pattern, not the goal. Auth + a per-user data table + one AI feature + deploy — that's the shape of 90% of small SaaS apps. Once you can run this loop with Claude Code, the next app takes you a third of the time. The AI feature is the part that varies; everything else is muscle memory.
The prompts in this chapter are deliberately high-level ("wire up magic-link auth", "add a notes table with RLS"), not step-by-step line-level instructions. That's the point: you've taught Claude your rules via CLAUDE.md, given it reusable skills, and framed every request with Plan → Implement → Self-audit → Ship. You direct; it codes.
Each phase ends with a concrete verify step. Don't skip them. "It seems to work" is how Chapter 9's silent bombs get planted.
- A Next.js app deployed on , reachable at a real URL.
- auth via . No passwords.
- A notes table with per-user — users only see their own.
- A "Summarise" button that calls Claude and returns a one-paragraph summary, cost tracked per user.
- A /feature-check-clean codebase that passes the Chapter 9 ship checklist.
Your idea → a master prompt
This is the skill nobody teaches. You already have a 1-line idea. Claude needs 5 sections. Every section is a pre-filled textarea — edit to improve, don't stare at a blank page. The seed text is deliberately slightly imperfect for your archetype, so your edits are the lesson.
Context
Tells Claude who this is for and why it exists. Without it, Claude has to guess the audience.
Build me a SaaS.
I'm building a tool for freelance designers to track which clients owe them money, as a private per-user app. Users sign in with magic links. It should feel like Linear — fast, keyboard-driven, no clutter.
Constraints
Rules Claude is not allowed to violate. This is where CLAUDE.md-style contracts go.
Write clean code.
- use pnpm (never npm) - TypeScript strict mode - RLS on every user-owned table - Zod safeParse on every public API route - never commit API keys - all server-side state lives in Supabase (no in-memory caches)
Stack
Explicit tech choices. Prevents Claude from reaching for defaults you don’t actually want.
Modern web stack.
Next.js 16 App Router, Supabase (auth + Postgres), @anthropic-ai/sdk for AI calls, TailwindCSS 4. Stripe Checkout if we need payments.
Deliverable
What must physically exist when Claude stops. Concrete artifacts, not behaviors.
A working app.
A running dev skeleton: - pnpm dev starts cleanly on localhost:3000 - CLAUDE.md at repo root (under 25 lines) - .env.local.example with all required vars - a smoke page at / showing "<app name> — coming soon" - git initialized with one "initial commit"
Acceptance
The checkpoints you’ll actually verify before calling the phase done.
It works.
- pnpm dev starts without errors - localhost:3000 renders the smoke page - CLAUDE.md mentions our 5 rules - git log shows one commit, git status is clean
[fill the capture form above]
[starts filling in as you edit the 5 sections above]Drop the copied text into .claude/commands/scaffold-mine.md so you never re-type it. Running /scaffold-mine will kick off your whole build from this master prompt.
Goal: get Next.js + a Supabase client + the Anthropic SDK talking to each other on localhost:3000. About 15 minutes.
Create a new Next.js app
Outside Claude Code, run this in a fresh directory:
pnpm create next-app@latest notes-ai --typescript --tailwind --app --src-dir --import-alias "@/*" cd notes-ai
Open the new folder in Claude Code (claude) from here on.
Drop in a CLAUDE.md
Before Claude writes a single file, give it the contract. Type this into the Claude Code session — not your terminal:
Create a CLAUDE.md at the repo root for [your idea] — a [your archetype]. Use the Next.js + Supabase + Anthropic starter template. Include hard rules for: - use pnpm, never npm - every anthropic.messages.create() must check isLoadTestMode() first - Zod safeParse on every public API route body - RLS must be enabled for every user-owned table - data model is [your data shape] Keep it under 25 lines.
Watch what Claude produces. Tweak it until it actually matches how you want to work.
Install the baseline dependencies
One prompt, one dependency set. Let Claude wire the imports as it goes:
Install these dependencies and wire up a basic client + server setup:
- @supabase/supabase-js and @supabase/ssr for auth + DB
- @anthropic-ai/sdk for AI calls
- zod for input validation
Add .env.local.example with all needed env vars (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY). Create src/lib/supabase/{client,server}.ts singletons.Verify it boots
Create a Supabase project (free tier) and an Anthropic API key, then fill in your and run the dev server:
cp .env.local.example .env.local # fill in the 4 values pnpm dev
Visit localhost:3000. You should see the default Next.js landing page. Phase 3 done.
Goal: a user can request a magic link, click it, and land signed in — and their first piece of data is protected by RLS. About 45 minutes, split across auth then the data model.
Write a memory file for auth BEFORE building
You're about to build auth. This is exactly the kind of thing future-you will want memory on. Do this first:
Create memory/MEMORY.md with an entry pointing to project_auth.md, and create memory/project_auth.md using the template from Ch 6. Fill the "Why this feature exists" and "Core invariants" sections based on these decisions: magic-link only (no passwords), 30-day session max, profiles table mirrors auth.users via a trigger.
If CLAUDE.md doesn't already point at memory, update it now.
Wire the auth surface
Build magic-link auth using Supabase. Create: - src/app/login/page.tsx — email input + "Send link" button, POSTs to /api/auth/magic-link - src/app/auth/callback/route.ts — handles the Supabase redirect, creates a session cookie - src/middleware.ts — redirects unauthenticated users from /app/* to /login - src/app/api/auth/magic-link/route.ts — validates email with Zod, calls supabase.auth.signInWithOtp() Every route handler uses Zod safeParse per our CLAUDE.md rules. Include loading/error states on /login.
Configure Supabase for local dev
Two browser-side steps. In your Supabase dashboard, open your project and navigate to Authentication → URL Configuration.
- Site URL: http://localhost:3000
- Redirect URLs: add http://localhost:3000/auth/callback
Test the flow
pnpm dev # visit localhost:3000/login # enter your email, click "Send link" # check inbox, click the link # you should land on localhost:3000/app (or whatever protected route exists)
Ask Claude to run /feature-check auth — it should catch anything missing (middleware not checking every protected route, sign-out not clearing cookies, etc.).
Goal: one user-owned table where every user only sees their own rows. Enforced by in the database — not your app code. The table name defaults to what you picked in the capture form.
Migration + RLS policies
Create a Supabase migration for [your idea] that: - adds public.[your_table] (id uuid pk, user_id uuid fk to auth.users, title text, body text, created_at timestamptz default now(), updated_at timestamptz default now()) - enables RLS on public.[your_table] - adds 4 RLS policies (select, insert, update, delete) — all scoped by auth.uid() = user_id - adds an index on user_id Data shape: [your data shape]. Put the migration in supabase/migrations/ and run it against my local dev DB.
CRUD API + list/detail pages
Build the [your_table] CRUD surface: - src/app/api/[your_table]/route.ts (GET list, POST create) — Zod-validated, uses the logged-in user's session - src/app/api/[your_table]/[id]/route.ts (GET, PATCH, DELETE) — RLS handles auth; return 404 on miss - src/app/app/page.tsx — list of my [your_table] with a "New" button - src/app/app/[your_table]/[id]/page.tsx — detail view with Edit / Delete buttons Use server actions where appropriate. Keep the UI minimal — we'll style later.
Verify RLS actually works
This is the non-skippable test. Open two browser profiles:
- Sign in as user A. Create two notes.
- Sign in as user B (separate email). Create one note.
- As B, visit /app— you should see B's one note only. Not A's.
- As B, try to curl A's note ID directly. You should get 404 / 403.
Goal: one AI-powered feature wired end-to-end. This chapter uses Summarise as the default — swap in whatever your archetype needs (generate / moderate / draft / auto-curate). About 45 minutes.
Prompt-from-file + load-test-guard
Create: - prompts/summarize.md — a system prompt that returns ONE paragraph (~50 words), plain language, no fluff - src/lib/prompts.ts — a loadPrompt() helper that reads the file and validates required tokens - src/lib/ai/helpers.ts — a cachedSystem() wrapper that wraps the prompt in cache_control: ephemeral - src/lib/load-test.ts — an isLoadTestMode() helper that returns true when process.env.LOAD_TEST === '1' Follow the prompt-from-file pattern from Ch 10's prompt contract.
The endpoint + cost tracking
Build POST /api/[your_table]/[id]/summarize: - Check auth; RLS handles ownership - If isLoadTestMode(), return a deterministic mock { summary: "LOAD TEST MOCK" } - Otherwise: load the [your_table] row body, call anthropic.messages.create with cachedSystem(SUMMARIZE_SYSTEM) - Log cost to a user_costs table (create migration too — simple: user_id, operation, cost_cents, created_at) - Return { summary: string } Export maxDuration = 30 so Vercel doesn't time out.
What will this AI feature cost per month?
| Line item | Math | USD / mo |
|---|---|---|
| Anthropic (claude-sonnet-4-7) | 10 users × 8 calls × (2000 in × $3/M + 400 out × $15/M) | $0.96 |
| Monthly total | AI only | $0.96 / mo |
Wire the UI button
On the [your_table] detail page: add a "Summarise" button. On click, POST to /api/[your_table]/[id]/summarize, show a loading state, render the returned summary in a small bordered box above the main body. If the POST fails, show the error in-place.
Sanity check before moving on
- Summary makes sense for the note body you wrote
- Run with LOAD_TEST=1 pnpm dev — confirm you get the mock response, not a billed call
- Check your Anthropic console — one successful call appears. Cost logged in the user_costs table
Then run /feature-check summarize. The 12 silent bombs from Ch 9 will all be considered. Anything flagged, fix now — not later.
Goal: the things every beginner skips that shipping actually requires — legal pages, error monitoring, analytics, and basic SEO. End with a realistic monthly cost estimate for your stack. About 30 minutes.
Legal pages (privacy + terms)
You are collecting emails. That alone means you need a privacy policy (GDPR / CCPA minimum), and terms if you're charging. These are not optional, but the template route is fine for v1.
Generate two pages for [your idea]: - src/app/privacy/page.tsx — a plain-language privacy policy. Cover: what data we collect (email for magic-link auth, user-owned rows in our database, minimal analytics), how long we keep it, third parties (Supabase for hosting, Anthropic for AI calls, our email provider), user rights (export, delete), and a contact email. - src/app/terms/page.tsx — terms of service. Cover: acceptable use, no warranty, liability cap, governing law placeholder (ask me to fill in my jurisdiction), termination. Data shape is [your data shape]. Monetization is [your monetization] (adjust terms accordingly). Keep each page under 250 words. Link both from the site footer.
Error monitoring with Sentry
When something breaks in prod, the first signal should not be a user tweeting at you. Sentry gives you a stack trace in your inbox within seconds.
pnpm add @sentry/nextjs npx @sentry/wizard -i nextjs --saas
Audit the Sentry wiring in this repo: - Confirm sentry.client.config.ts, sentry.server.config.ts, and sentry.edge.config.ts exist - Ensure NEXT_PUBLIC_SENTRY_DSN is in .env.local.example - Tag every captureException with user.id (when authed) and the operation name - For the [your idea] app specifically, wrap the AI endpoint in Sentry.startSpan so slow calls surface as performance issues Show me the diff before applying.
Analytics (Plausible default)
A single pageview script that respects privacy, doesn't need a cookie banner, and doesn't slow the site down. Plausible is the default here; the alternatives GoDeeper covers Posthog (for product analytics) and GA4 (free but heavier).
Wire Plausible Analytics into src/app/layout.tsx: - Script tag with defer, data-domain set to the prod domain (read from NEXT_PUBLIC_SITE_URL) - Only include in production (skip on localhost) - No cookies, no consent banner needed - Add a /api/health endpoint that Plausible can hit for uptime The app is [your idea] — a [your archetype]. Prefer lightweight over feature-rich.
SEO essentials
You are not going to rank for generic keywords. But people who land on your page via Twitter, a friend's share, or a Product Hunt launch should see a real page title, a real meta description, a real Open Graph image. That means: sitemap, robots.txt, and a metadata object on every route.
Add SEO essentials to the [your idea] app: - src/app/sitemap.ts (Next.js-native) listing every public route - src/app/robots.ts allowing all, pointing at sitemap - A Metadata export on every public route (landing, /privacy, /terms) with title, description tailored to [your idea], and an og:image reference - A single /public/og-image.png placeholder (1200x630) — tell me what tool to generate one in - Update src/app/layout.tsx Metadata base with the prod domain Keep titles under 60 chars and descriptions under 160 chars.
How much will this cost you monthly?
Now that you know the pieces (Vercel + Supabase + Anthropic + domain + maybe Sentry), look at the realistic monthly cost at your expected user scale. This is the number to keep in your head when you're thinking about pricing.
Your full monthly stack cost
| Line item | Math | USD / mo |
|---|---|---|
| Anthropic (claude-sonnet-4-7) | 10 users × 8 calls × (2000 in × $3/M + 400 out × $15/M) | $0.96 |
| Vercel · Hobby | 100 GB-hours, 100k serverless invocations, 100 GB bandwidth | $0.00 |
| Supabase · Free | 500 MB database, 1 GB file storage, 50k monthly active users | $0.00 |
| Monthly total | Vercel + Supabase + Anthropic | $0.96 / mo |
Goal: live on a real URL, real magic-link auth, a first real user able to sign up. About 30 minutes (+15 if you're pointing a custom domain at it).
Pre-flight checklist
Run /ship notes-ai. This should chain: - /feature-check across auth, notes, and summarize - /theme-check on the diff (if you support light mode) - /security-audit (secrets, CORS, RLS) Fix any blockers. Don't deploy on yellow.
Deploy to Vercel
Guide me through deploying to Vercel: 1. Push to a new GitHub repo 2. Import to Vercel (dashboard) 3. Set env vars: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY 4. Deploy 5. Tell me what to add to Supabase Auth → URL Configuration so magic-link redirects work on the prod domain
Update Supabase for production
Back in the Supabase dashboard:
- Go to Authentication → URL Configuration.
- Update Site URL to your Vercel production domain (e.g. https://notes-ai.vercel.app).
- Add your production /auth/callback path to Redirect URLs.
Point a custom domain at it (optional)
Vercel's <project>.vercel.appis fine for day one, but a real domain is ~$12/year and the single biggest credibility bump you'll make. Buy it from a good registrar (Porkbun or Cloudflare — avoid GoDaddy), then point DNS at Vercel.
- In Vercel → your project → Settings → Domains → add your new domain. Vercel shows you the exact DNS records it needs.
- In your registrar's DNS panel, add either:
- An A record on @pointing to Vercel's IP (shown in the dashboard), or
- A CNAME on www pointing to cname.vercel-dns.com.
- Wait for propagation (usually a few minutes; up to 24h in worst cases). Vercel auto-provisions the SSL cert once DNS resolves.
- Update Supabase Site URL + Redirect URLs again — this time with the new custom domain — or magic links will send users to the .vercel.app URL.
- Update NEXT_PUBLIC_SITE_URL env var in Vercel to the new canonical domain and redeploy.
The smoke test that matters
In an incognito window on your phone or a different machine:
- Sign up with a fresh email.
- Create a row.
- Hit your AI feature button. Confirm a real response comes back.
- Sign out, sign back in — your data is still there. (Confirms auth + DB wiring survives a clean session.)
If every step works: you shipped. Tick Phase 7. Close the laptop. Text a friend.
You picked the MVP path (free, no payments in v1). That's the right call for validating an idea — you'll learn more from shipping and watching real users than from a pricing page. Monetization is a separate skill, worth learning deliberately when you have a product to charge for.
When you're ready, change the capture form's monetization field at the top of the chapter, and this phase will unlock the full Stripe walkthrough.
Every primitive used, real app shipped
Zoom out at what just happened: in five phases you used every primitive in the course. gave Claude your rules. preserved auth decisions. /plan kept architecture from drifting. /feature-check caught silent bombs. Hooks (if you wired any) stopped bad edits cold. Skills made the repeatable bits repeatable.
Chapter 9 is next — a 12-bomb sanity check you'll run against the thing you just built (and every future app). It's short. Then Chapter 10 is optional advanced material if you want to go deeper.