Case Study: SaaS MVP
Background
Two founders — one ex-facility-ops, one full-stack — building FitMaintain: SaaS for gym chains to schedule equipment maintenance, log technician visits, and bill locations per machine. Stack locked early: Next.js App Router, Fastify API, Postgres, Stripe. Target: first paying gym in three weeks. Neither had shipped with coding agents before; both had shipped products the slow way.
They had a Friday investor coffee. The agent session started Thursday night.
Initial vague prompt
Build a SaaS for gym equipment maintenance. Next.js frontend, Fastify
backend, Postgres, Stripe. Users should manage gyms, equipment, schedule
maintenance, and pay. Make it look polished — we have a demo tomorrow.Claude Code ran for ~90 minutes. It produced a monorepo, Prisma schema, auth pages, a dashboard with charts, Stripe Checkout wiring, and seed data for "Iron Temple Fitness." The UI looked like a product. They slept.
Why it failed
Friday morning, the demo died in the first five minutes:
| Click | What happened |
|---|---|
| Create gym | 500 — gyms.owner_id NOT NULL, seed user had no row in users after a schema drift the agent "fixed" overnight |
| Schedule maintenance | Form submitted; row appeared; technician assignment was a free-text string, not a user FK |
| Stripe "Subscribe" | Checkout session created against test keys hardcoded in the client bundle |
| Refresh dashboard | Charts showed hardcoded JSON from lib/demoMetrics.ts — not queries |
They had a 6-hour demo, not an MVP. Rough accounting afterward: ~2% of the surface area a paying gym would need, dressed as 80%. The agent optimized for visual completeness under a vague prompt. That is the correct local optimum for "make it look polished." It is the wrong optimum for "ship something that survives a second click."
Restarting with momentum ("just fix the bugs") would have compounded the lie. They wiped apps/ and kept only the repo rails: CI, CLAUDE.md, empty schema, deploy to Fly.
The improved spec
They wrote specs/PLAN.md and three slice specs before the next agent session. Abridged from specs/003-schedule-maintenance.md:
## Slice 003 — Schedule maintenance visit
### Done when
- Authenticated owner can POST /v1/gyms/:id/visits with {equipmentId, windowStart, windowEnd}
- Visit appears in GET /v1/gyms/:id/visits
- equipmentId must belong to that gym (403 otherwise)
- Overlapping windows for same equipment rejected (409)
- Next.js page at /gyms/[id]/visits creates and lists visits
- Integration test covers happy path + overlap + cross-gym equipment
### Out of scope
- Technician assignment, notifications, Stripe, PDF reports, charts
### Quality tier
- Auth: production (already shipped in 001)
- Billing: stub — do not touch Stripe
- Email: console logger onlyQuality tiers lived in PROJECT_CONTEXT.md so the agent could not "helpfully" finish Stripe mid-slice. See Specs over Prompts and the MVP Build playbook.
Agent workflow
Each session opened with an explicit instruction to read CLAUDE.md, PROJECT_CONTEXT.md, and the slice spec — and to list adjacent problems without fixing them. Sessions that tried to "also clean up the dashboard" were reverted.
Stripe stayed a FakeBillingProvider until slice 009, after visits and equipment CRUD were verified on the deployed skeleton. The investor coffee was rescheduled; the first gym signed a pilot in week three.
Human intervention points
- After the wipe — founders wrote PLAN.md themselves. The agent was not asked to invent the product shape.
- Between every slice — one founder ran the "Done when" checklist by hand on the Fly URL, not localhost.
- Before Stripe — they reviewed the billing interface and webhook signature verification personally; the agent implemented against a frozen interface.
- Before pilot — they deleted
lib/demoMetrics.tsand any seed that faked aggregates. If a number was on screen, it came from SQL.
Mistakes made
- Trusting visual completeness. Cost: one lost demo day + two days of emotional recovery before the restart. The charts were the tell; nobody asked "where does this number come from?"
- Hardcoded secrets in the first agent pass. Test Stripe keys in
NEXT_PUBLIC_*. Caught only because the second founder grepped the client bundle. Would have been a production incident if they had "fixed forward." - One mega-session on the restart attempt (day 8). Context rot: the agent "remembered" a previous schema that no longer existed and spent 40 minutes fighting Prisma migrate. Fix: one slice per session, always.
- Skipping characterization of the fake billing stub. Slice 009 assumed the stub's
charge()signature; an earlier session had renamed it. Two hours of thrash until they pinned the interface inPROJECT_CONTEXT.md.
Final outcome
Week 3 deliverable: one gym org, equipment CRUD, visit scheduling with overlap rules, invite-only auth, Stripe subscription for the org (not per-machine — deferred), deployed, CI green, 47 integration tests. No charts. The pilot gym's ops manager used it for two weeks before asking for technician assignment — which became slice 012 under a new spec, not a hallway prompt.
Line counts at pilot: ~6.2k LOC application code, ~1.8k LOC tests. Roughly 70% agent-typed, 100% human-gated.
Lessons
- A polished demo under a vague prompt is evidence of agent competence at UI theater, not product progress. Measure slices verified end-to-end on the real deploy target.
- Record quality tiers in writing before the agent touches billing, auth, or email — otherwise it will silently harden or silently stub based on whatever file it saw last.
- Wipe and restart when the artifact is a lie. Fixing forward from fake charts and client-side secrets costs more than a clean walking skeleton.
- Fresh session per slice beats a heroic overnight session; context rot looks like the agent being "bad at Prisma."
- Investor demos are a forcing function for the wrong optimum. Move the date before you move the definition of done.
Related
- Playbook: MVP Build — walking skeleton, quality tiers, one slice per session
- Specs over Prompts — why PLAN.md replaced the Thursday-night prompt
- Shallow Implementation — the demo-metrics failure mode named
- Responsibility Split — what founders keep vs what agents type
- New Project — rails before slices