Taking an AI-built app to production
A non-technical founder prompt to headless Claude Code produced a working Next.js + Postgres supplier
portal. Its first pass was fairly careful — so I reverted the shipped before/ app to the
common vibe-coded failure classes to represent the real rescue market, proved each one with an automated
exploit, then hardened after/ and re-ran the same exploits.
Tallgrass Precision Components is fictional · demo by Garrett Smith
| Severity | Findings | Fixed in after |
|---|---|---|
| Critical | 4 | 4 |
| High | 4 | 4 |
| Medium | 4 | 4 |
| Low | 1 | 1 |
Generated from tests/results.json on 2026-10-07 by tests/run.mjs.
Findings — before vs after
E-002 · IDOR: a supplier can read another supplier’s purchase order
E-005 · Auth bypass: forge an unsigned session cookie
E-006 · SQL injection in the part-number search
E-003 · Secret shipped in the client bundle (NEXT_PUBLIC service key)
E-004 · Open CORS: wildcard origin with credentials
E-009 · Passwords stored in plaintext
E-011 · Upload validation bypass → stored XSS via certificate content-type
E-007 · No rate limiting on login (credential brute force)
E-008 · Verbose errors leak stack traces and SQL
E-010 · Missing security headers (no CSP / HSTS / frame protection)
E-013 · Insecure deploy config: secrets committed, no health check
E-012 · N+1 queries on the dashboard list
Functional tests (hardened app still works)
| ID | Test | Result | Evidence |
|---|---|---|---|
| FN-1 | Valid supplier login succeeds | PASS | login HTTP 200 |
| FN-2 | Wrong password is rejected | PASS | HTTP 401 |
| FN-3 | Supplier sees only their own company’s POs | PASS | 4 POs, suppliers: Ridgeline Aerospace Machining |
| FN-4 | Buyer sees all suppliers’ POs | PASS | 10 POs across 3 suppliers |
| FN-5 | Search by part number works | PASS | 1 match "INC" |
| FN-6 | Supplier can send a message on their own PO | PASS | HTTP 201 |
| FN-7 | Valid PDF upload is accepted; non-PDF is rejected | PASS | pdf:201 non-pdf:400 |
| FN-8 | Health check reports OK | PASS | HTTP 200 {"status":"ok","db":"up","time":"2026-10-07T07:35:15.358Z"} |
Production hardening checklist
Production hardening checklist — AI-built / prototype web apps
A reusable checklist I run when taking a prototype (Lovable, v0, Bolt, Cursor, a
weekend Next.js + Postgres/Supabase build) to production. Ordered by how often the
item is the thing that actually gets a prototype breached. Each item: what to check,
how to check it fast, and the fix.
Legend: C critical / ship-blocker · H high · M medium · L low
1. Authentication & session
- C Every mutating and data-returning API route checks an authenticated session.
Grep for route handlers (app/api/**/route.*, Express routes) and confirm each one
resolves a *server-verified* identity before touching data. Fix: one requireUser()
helper called at the top of every handler; deny by default.
- C Session tokens are signed/encrypted and verified server-side. Reject any scheme
that trusts a raw userId/role from a cookie, header, or request body. Test: tamper
the cookie to another user's id and replay. Fix: signed JWT (jose) or encrypted session
(iron-session), verified on every request; never read identity from client input.
- H Passwords hashed with a slow algorithm. No plaintext, MD5, SHA-1, or unsalted
hashes. Fix: bcrypt/argon2 (cost ≥ 12). Check the users table and the login/signup code.
- H Privilege/role comes from the server record, not the client.
role=buyerin a
cookie or body must never grant buyer access. Fix: load role from the DB row for the
verified user id.
- M Session expiry + logout actually invalidate. Short-lived tokens, refresh on the
server, logout clears the cookie.
2. Tenant isolation & authorization (RLS)
- C Row-Level Security enabled on every tenant-scoped table (Supabase: CVE-2025-48757).
`SELECT relname, relrowsecurity FROM pg_class JOIN pg_namespace n ON n.oid=relnamespace
WHERE n.nspname='public' AND relkind='r'; — any relrowsecurity = false` on a table with
tenant data is a finding. Fix: ALTER TABLE t ENABLE ROW LEVEL SECURITY; + explicit
policies. Enabling RLS with no policy denies all (good default); forgetting to enable
it exposes everything.
- C No IDOR: object access is scoped to the caller's tenant. For every
/:idroute,
request another tenant's id while authenticated as tenant A. Fix: either DB-enforced
(RLS + per-request SET LOCAL of the tenant claim) or an explicit WHERE tenant_id = $me
on every query. Prefer DB-enforced — app-layer checks get missed on the next new route.
- H The app does not connect as a superuser / service-role for user-facing queries.
A service-role connection bypasses RLS entirely. Fix: a limited DB role for request-path
queries; reserve the service role for migrations/admin jobs only.
- M List endpoints filter by the server-known tenant, never by a client-supplied id.
3. Secrets
- C No secret shipped to the browser. Build the client bundle and grep it for key
material: grep -rE 'sk-|service_role|AKIA|-----BEGIN|password|secret' .next/static dist.
In Next.js, only NEXT_PUBLIC_* reaches the client — a secret must never carry that prefix.
Fix: move it server-side; rotate any key that was ever public.
- C No secrets committed.
git log -p | grep -iE 'api[_-]?key|secret|password'; check
.env is git-ignored and an .env.example with placeholders is committed instead.
- H Keys are rotatable and scoped (separate keys per environment; least privilege).
4. Input validation
- C No SQL injection. All queries parameterized; grep for string-built SQL
(template literals / + with user input into query(...)). Fix: parameterized queries
($1), or a query builder. Never interpolate req values into SQL.
- H Every input validated against a schema at the trust boundary. Fix:
zod(or
equivalent) parse at the top of each route; reject on failure with a 400. Validate type,
length, format, and enum membership — not just presence.
- H File uploads validated: allow-list content type, cap size, generate a random stored
name (never trust the client filename → path traversal), store outside the web root or in
object storage, and gate downloads by authorization.
- M Output encoding / no reflected XSS; avoid
dangerouslySetInnerHTMLwith user data.
5. CORS
- H CORS is not
*with credentials.Access-Control-Allow-Origin: *combined with
cookies is both invalid and a leak vector; a reflected-origin + Allow-Credentials: true
is worse. Fix: an explicit allow-list of known origins, or same-origin only (no CORS
headers at all for a same-origin app).
6. Rate limiting & abuse
- H Auth and expensive endpoints are rate-limited. Test: 20 rapid login attempts —
expect 429s / backoff. Fix: a limiter (Upstash/Redis in prod; in-memory token bucket for
single-instance) keyed by IP + account; add lockout/backoff on repeated login failure.
- M Pagination / payload caps so a single request can't pull the whole table.
7. Security headers & CSP
- M Core headers set on every response:
Content-Security-Policy,
X-Content-Type-Options: nosniff, X-Frame-Options: DENY (or CSP frame-ancestors),
Referrer-Policy, Strict-Transport-Security (prod/HTTPS). Fix: set in middleware or
next.config headers. Check with curl -sI.
8. Error handling
- H No stack traces / DB errors returned to clients. Trigger a failure and inspect the
response body. Fix: catch at the route boundary, log the detail server-side with a
correlation id, return a generic message + id to the client. Set a framework-level error
boundary / 500 page.
9. Logging & monitoring
- M Server-side structured logs for auth events, authorization denials, and errors —
without logging secrets, passwords, or full PII. A correlation id ties a client error to a
log line. Wire an error reporter (Sentry or equivalent) for prod.
10. Backups & data safety
- M Automated DB backups + a tested restore. Know the RPO/RTO. For Supabase/managed PG,
confirm point-in-time recovery is on. Destructive migrations reviewed; no DROP without a backup.
11. Deploy & environment
- H Production config exists and is separate from dev. Not localhost-only: binds
0.0.0.0, real NODE_ENV=production build, env vars validated at boot (fail fast on a
missing secret), HTTPS/TLS terminated, and a /health (or /api/health) endpoint for the
platform's checks. Next.js: output: 'standalone' or a static export as appropriate.
- M No test/seed/admin backdoor data in prod; default/demo passwords removed.
12. Dependencies
- M
npm audit(prod deps) clean of high/critical, lockfile committed, and
unmaintained/abandoned packages replaced. Re-run in CI.
How I use this
I walk the list top-to-bottom against the running app and the repo, record each item as
pass / finding, and turn every finding into: id, severity (CVSS-style rationale),
evidence (file:line + a request/response), a runnable exploit that proves it, and the fix.
The critical tier (auth, RLS/tenant isolation, secrets, SQLi) is what actually gets
prototypes breached — I clear those before anything cosmetic.
_Maintained by Garrett Smith. Built for the Tallgrass Precision Components (fictional) rescue demo._
Real renders




