An app that works on your laptop is not a production system. AI coding tools are good at the happy path and weak at everything around it: access control, secrets, failure handling, backups and monitoring. This guide is a reference checklist of 73 checks in 13 areas. Each area explains why it matters, gives the checks, and shows code where code helps. Every check maps to a public standard (OWASP, the Twelve-Factor App, Google SRE, web.dev), so you can verify it yourself instead of trusting us.
Share of 80 coding tasks where AI-written code introduced an OWASP Top 10 vulnerability (Veracode benchmark).
Broken Access Control, still the number one risk in the OWASP Top 10:2025.
Largest Contentful Paint a page should hit to count as good on web.dev.
Source: Veracode: 2025 GenAI Code Security Report. This is a vendor benchmark of 80 prompt-style tasks scanned with Veracode's own tool. It shows how often models write vulnerable code in those tasks, not how many production apps are vulnerable.
What does production-ready mean for an AI-built app?
Production-ready means the app stays safe, correct and recoverable when real users, real money and real attackers arrive. It is a different bar from "it runs". Use the checklist below in three passes:
- Blocker: do not launch until it is done. These are marked [B].
- Week one: fix within seven days of launch, with a dated owner. Marked [W1].
- Hardening: schedule it. Marked [H].
If you only have an hour, do every [B] item. That is the launch gate at the end of this post.
Why do AI-built apps fail in production?
The same failure modes keep appearing, and each one maps to a known category.
| What breaks | Real example | OWASP Top 10:2025 category |
|---|---|---|
| Any logged-in or anonymous user can read other people's rows | CVE-2025-48757: weak Row Level Security in Lovable-generated apps let unauthenticated users read or write tables using the public key | A01 Broken Access Control |
| Secrets in code, open admin panels, debug routes left on | Keys committed to Git, test endpoints shipped | A02 Security Misconfiguration |
| Unchecked packages and build steps | AI suggests a library you never verified | A03 Software Supply Chain Failures |
| Weak or home-made login | Passwords stored with a fast hash or in plain text | A04 Cryptographic Failures, A07 Authentication Failures |
| User input reaches the database or the page unchanged | SQL injection, cross-site scripting | A05 Injection |
| An agent with write access to the live database | An AI agent deleted a production database during a code freeze in 2025 | A06 Insecure Design |
| Nobody notices an attack or an outage | No logs, no alerts | A09 Security Logging and Alerting Failures |
| Errors crash the app or leak stack traces | Unhandled exceptions | A10 Mishandling of Exceptional Conditions |
Sources: NVD-based summary of CVE-2025-48757 at SentinelOne; Superblocks: Lovable RLS breach, 170+ apps, a vendor write-up; OECD AI Incident: Replit deletes production database. The Replit details come from screenshots shared by the user involved and press reports. The founder of Replit called the event unacceptable and announced separate dev and production databases.
The full category list is on the OWASP Top 10:2025 page. The 13 areas below cover all ten.
Area 1: Source code and dependencies
AI tools install packages freely and leave scaffolding behind. Treat every dependency as code you are shipping.
- A1 [B] The project is in Git, with a clean history and a protected
mainbranch. - A2 [B] You commit before every large AI change, so you can roll back to a known good version.
- A3 [W1] A lockfile is committed (
package-lock.json,pnpm-lock.yamloruv.lock), and CI installs withnpm ci, notnpm install. - A4 [W1]
npm audit(or your language's equivalent) runs in CI, and automated dependency updates such as Dependabot are on. - A5 [B] You checked that every package the AI suggested exists, has a real publisher and a believable download history. AI tools sometimes name packages that do not exist, and attackers can register those names.
- A6 [B] Demo routes,
/api/test, seed users, "magic" login links and debug flags are removed.
# Run before every release npm ci npm audit --omit=dev npx tsc --noEmit npm run build
Area 2: Secrets and configuration
The Twelve-Factor App config rule gives a simple test: could you open-source your repository today without leaking a credential? If not, config is still mixed into code.
- B1 [B] No API key, token or password appears in source files. Search the repo, including old commits.
- B2 [B]
.env*files are listed in.gitignore. - B3 [B] In Next.js only values meant to be public carry the
NEXT_PUBLIC_prefix. Anything with that prefix is bundled into browser code and readable by every visitor. Next.js lists this in its production checklist. - B4 [B] Development, staging and production use different keys. A leaked dev key must not open production.
- B5 [W1] GitHub push protection or another secret scanner blocks commits that contain keys. See GitHub: about push protection.
- B6 [W1] The app validates its environment variables at startup and refuses to boot if one is missing.
// src/env.ts
import { z } from 'zod';
const schema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
NEXT_PUBLIC_SITE_URL: z.string().url(),
});
export const env = schema.parse(process.env); // throws at boot, not at 2 a.m.
What if a secret was already committed?
Deleting the line does not remove it from Git history. Follow this order.
- 01
Revoke or rotate the key first
Create a new key at the provider and disable the old one. Once revoked, the leaked value cannot be used.
- 02
Check the provider's logs
Look for use of the old key from addresses you do not recognise, and for unexpected charges.
- 03
Purge it from history if needed
GitHub recommends git-filter-repo with the sensitive-data-removal flag, then a force push. Forks keep the old history, so check them too.
- 04
Add a scanner so it cannot recur
Turn on push protection and a pre-commit secret check.
Source: GitHub Docs: Removing sensitive data from a repository.
Area 3: Authentication
Do not let an AI invent a login system. Authentication is a solved problem, and the safest code is code you did not write.
- C1 [B] Sign-in uses a maintained provider or library (for example Auth.js, Clerk, Supabase Auth or your framework's official option).
- C2 [B] If you must store passwords yourself, use Argon2id. OWASP's minimum is 19 MiB of memory, 2 iterations and 1 degree of parallelism. Use bcrypt with a work factor of at least 10 only on legacy systems.
- C3 [B] Session cookies are set
HttpOnly,SecureandSameSite=LaxorStrict. - C4 [W1] Login, sign-up and password reset are rate limited per IP and per account.
- C5 [W1] Password reset and email verification links expire and can be used once.
- C6 [H] Admin accounts require multi-factor authentication.
Source: OWASP Password Storage Cheat Sheet.
Area 4: Authorization and data access
This is the area that matters most, because Broken Access Control is OWASP's number one risk. Authentication asks who you are. Authorization asks what you are allowed to touch. AI-generated code often gets the first right and skips the second.
- D1 [B] Every database read and write is scoped to the signed-in user, on the server.
- D2 [B] If you use Supabase or another database exposed to the browser, Row Level Security is enabled on every table in an exposed schema, and each table has a policy.
- D3 [B] The service-role or secret key never reaches the browser. It bypasses Row Level Security by design.
- D4 [B] Each server action and API route checks authentication and permission itself. Next.js says not to rely on layout or page-level checks alone.
- D5 [B] Admin routes check a role on the server, not a hidden button in the interface.
- D6 [B] You tested with two accounts that user A cannot open user B's records by changing an ID in the URL or request body. This bug is called an insecure direct object reference (IDOR).
-- Supabase / Postgres: owner-only access alter table public.reports enable row level security; create policy "Owners can read their reports" on public.reports for select to authenticated using ( (select auth.uid()) = user_id );
Source: Supabase Docs: Row Level Security. A table with RLS on and no policy returns nothing, which is the safe failure. A table with RLS off in an exposed schema can be read and written by any role with a grant.
// Next.js server action: check inside the action, not only in the page
'use server';
import { z } from 'zod';
const Input = z.object({ reportId: z.string().uuid() });
export async function deleteReport(raw: unknown) {
const { reportId } = Input.parse(raw);
const session = await getSession(); // your auth helper
if (!session) throw new Error('Unauthorized');
// Scope the query to the owner, so a guessed ID deletes nothing
const result = await db.report.deleteMany({
where: { id: reportId, userId: session.userId },
});
if (result.count === 0) throw new Error('Not found');
}
Area 5: Input validation and injection
Everything that comes from outside your server is untrusted: form fields, query strings, headers, uploaded files and webhook bodies.
- E1 [B] Every endpoint validates its input on the server with a schema (Zod, Valibot, Pydantic). Client-side checks are for user experience only.
- E2 [B] Database access uses parameterized queries or an ORM. OWASP lists prepared statements first among its SQL injection defenses and strongly discourages manual escaping.
- E3 [B] The app never builds HTML from user text. Avoid
dangerouslySetInnerHTML, or sanitize the input first. - E4 [W1] Uploads have a size limit, an allowed-type list, a random file name and storage outside the web root.
- E5 [W1] Any URL the server fetches for a user (image proxy, link preview) is checked against an allow-list, to block server-side request forgery.
// Unsafe: string building
const rows = await db.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// Safe: parameterized
const rows = await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
Source: OWASP SQL Injection Prevention Cheat Sheet.
Area 6: Browser and transport hardening
- F1 [B] The whole site is served over HTTPS and HTTP redirects to HTTPS.
- F2 [W1] Security headers are set. OWASP's recommended values are in the snippet below.
- F3 [H] A Content Security Policy limits where scripts can load from. Start in report-only mode so it does not break the site.
- F4 [B] CORS allows only your own origins, never
*together with credentials. - F5 [W1] State-changing requests are protected against cross-site request forgery. Next.js server actions and SameSite cookies cover most cases. Check any custom route.
// next.config.js
const securityHeaders = [
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'geolocation=(), camera=(), microphone=()' },
{ key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
Source: OWASP HTTP Headers Cheat Sheet. The preload flag in HSTS is a long commitment, so add it only when every subdomain is on HTTPS.
Area 7: Payments and webhooks
Money flows are where a small bug becomes a real loss. These rules apply to Stripe and most payment providers.
- G1 [B] The price comes from your server or the provider, never from a value the browser sends.
- G2 [B] Webhook endpoints verify the signature using the raw request body. Parsing the body first breaks verification.
- G3 [B] Live and test keys are separate, and the live site uses live keys only.
- G4 [B] Webhooks are idempotent: processing the same event twice does not ship two orders. Stripe says duplicates can occur and tells you to log processed event IDs.
- G5 [W1] The handler returns a
2xxstatus quickly and does slow work in a queue, because delivery timeouts trigger retries. - G6 [W1] You do not depend on event order. Stripe does not guarantee it, so fetch the current object when in doubt.
// app/api/stripe/webhook/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text(); // raw body, do not JSON.parse first
const signature = req.headers.get('stripe-signature');
if (!signature) return new Response('Missing signature', { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response('Invalid signature', { status: 400 });
}
// Idempotency: the primary key rejects a second insert of the same event
const first = await recordEventOnce(event.id); // see SQL below
if (!first) return new Response('Already processed', { status: 200 });
await queue.enqueue(event); // slow work happens elsewhere
return new Response('ok', { status: 200 });
}
create table processed_events ( id text primary key, received_at timestamptz not null default now() ); -- insert ... on conflict (id) do nothing; if 0 rows inserted, it was a duplicate
Source: Stripe Docs: Receive Stripe events in your webhook endpoint.
Area 8: AI features inside your app
If your product calls a language model, you have a second attack surface. The OWASP Top 10 for LLM Applications 2025 names the risks. These five come up most in small products.
- H1 [B] Treat all model input as hostile (LLM01 Prompt Injection). A web page, email or PDF the model reads can carry instructions. Never let retrieved text change what the app is allowed to do.
- H2 [B] Give the model the least power possible (LLM06 Excessive Agency). Read-only tools by default. Anything that sends, pays, deletes or posts needs a human approval step. Our guide to AI action guardrails shows how.
- H3 [B] Treat model output as untrusted (LLM05 Improper Output Handling). Validate it against a schema before it reaches a database, a shell, a URL or a web page.
- H4 [B] Spend is capped (LLM10 Unbounded Consumption). Set a hard monthly limit at the model provider, and a per-user rate limit and token cap in your app. One abusive user or a loop bug should not produce a surprise bill.
- H5 [W1] No secrets in the system prompt (LLM07 System Prompt Leakage). Assume users can read it. Keep keys and business rules on the server.
- H6 [W1] You know what personal data you send to the model vendor, and your privacy policy says so (LLM02 Sensitive Information Disclosure).
// Validate model output before acting on it
const Action = z.object({
type: z.enum(['draft_reply', 'tag_lead']), // no "send_email", no "delete"
text: z.string().max(2000),
});
const parsed = Action.safeParse(JSON.parse(modelOutput));
if (!parsed.success) return fallbackToHuman();
Area 9: Data, environments and backups
- I1 [B] Development, staging and production are separate environments with separate databases. Any AI coding agent works on development only.
- I2 [B] Agents and automation tools hold the smallest permissions they need. No agent gets a credential that can drop production tables.
- I3 [B] Automatic backups are on, and you have restored one into a test database. A backup you never restored is a hope.
- I4 [W1] Schema changes are versioned migrations in Git, not manual edits in a dashboard. Each migration has a rollback plan.
- I5 [W1] You know your recovery goals: how much data you can afford to lose (recovery point) and how long you can be down (recovery time). Write them down, even as rough numbers.
- I6 [H] You have a deletion and retention plan for personal data, so a user's "delete my account" request has a real procedure.
Area 10: Testing and continuous integration
AI-written code usually passes the happy path and fails at the edges. Test the edges on purpose.
- J1 [B] You tried the unhappy paths by hand: wrong password, empty form, expired session, duplicate submit, a payment that fails, a slow network.
- J2 [W1] CI runs type checks, lint, unit tests and a production build on every pull request, and a failing job blocks the merge.
- J3 [W1] One automated end-to-end test covers your money or sign-up flow from start to finish.
- J4 [H] A simple load test shows the app survives ten times your expected day-one traffic.
# .github/workflows/ci.yml (minimal)
name: ci
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npx tsc --noEmit
- run: npm run lint
- run: npm test --if-present
- run: npm run build
Area 11: Observability and incident response
You cannot fix what you cannot see. The Google SRE book defines four golden signals to monitor for any user-facing system.
| Signal | What it measures | A simple starting alert |
|---|---|---|
| Latency | Time to serve a request, tracking failed requests separately | Slow responses above your target for 5 minutes |
| Traffic | Demand on the system | A sudden drop to near zero, or a sharp spike |
| Errors | Rate of failed requests, explicit or implicit | Error rate above a small threshold |
| Saturation | How close the most constrained resource is to its limit | Database connections or memory near the cap |
- K1 [B] An error tracker such as Sentry captures server and browser exceptions with the release version.
- K2 [B] An uptime check hits a health endpoint every minute and alerts a person by email, SMS or chat.
- K3 [W1] Alerts go to a human who will act, not to an unread inbox. Name who.
- K4 [W1] Logs are structured, include a request ID, and never contain passwords, tokens or full card data.
- K5 [W1] You monitor business signals too: sign-ups, payments and emails sent. An app can return
200 OKand still be wrong. We explain this trap in agents that return 200 OK and are still wrong. - K6 [H] A short runbook says what to do for the three most likely failures: site down, payments failing, database full.
Area 12: Performance, errors and discoverability
- L1 [W1] Core Web Vitals meet the "good" thresholds at the 75th percentile: Largest Contentful Paint at or under 2.5 seconds, Interaction to Next Paint at or under 200 milliseconds, Cumulative Layout Shift at or under 0.1. See web.dev: Web Vitals.
- L2 [W1] Run
next buildandnext startlocally and test Lighthouse in an incognito window before release, as the Next.js checklist suggests. Lighthouse is a lab test, so confirm with field data later. - L3 [B] Custom 404 and error pages exist, so a crash shows a calm message and never a stack trace.
- L4 [H] Images use the framework image component, fonts are self-hosted, and third-party scripts are deferred.
- L5 [W1]
robots.txt, a sitemap, page titles, descriptions and share images are set, so search engines and link previews work from day one.
Area 13: Launch day, rollback and legal basics
- M1 [B] You know how to roll back. On Vercel and similar hosts that means redeploying the previous build. Test it once before launch, with a stopwatch.
- M2 [W1] Risky features sit behind a flag, so you can switch them off without a deploy.
- M3 [B] A privacy policy and terms of service are live and match what the app really collects. Cookie consent is added where your audience's law requires it. Have a qualified professional review them if you handle sensitive data.
- M4 [B] Domain, DNS and TLS certificate renewals are automatic, and the renewal email goes to a monitored address.
- M5 [W1] A status or contact page tells users where to report a problem.
- M6 [W1] You launch to a small group first, watch the dashboards for a day, then open the doors.
Time how long it takes to restore the previous version. If it takes more than five minutes, fix the process before launch day.
The launch gate: what must be green before you go live
Print this table. Do not launch with any blocker open.
| Gate | Checks that must pass | Why this one |
|---|---|---|
| Code is recoverable | A1, A2, A5, A6 | You can undo mistakes and nothing risky shipped by accident |
| Secrets are private | B1, B2, B3, B4 | One leaked key can empty an account |
| Login is trusted | C1, C2, C3 | Never ship home-made authentication |
| Data is isolated | D1, D2, D3, D4, D5, D6 | Broken access control is the top OWASP risk |
| Input is safe | E1, E2, E3 | Injection is preventable with standard patterns |
| Transport is secure | F1, F4 | Basic hygiene for every site |
| Money is correct | G1, G2, G3, G4 | Payments need integrity and idempotency |
| AI is bounded | H1, H2, H3, H4 | Limits agency and spend |
| Data is recoverable | I1, I2, I3 | A tested backup is your last defense |
| Failures are visible | J1, K1, K2, L3 | You find out before your users do |
| You can retreat | M1, M3, M4 | Rollback, legal basics and renewals |
If you are short on time, the order of risk is: access control (Area 4), secrets (Area 2), backups (Area 9), payments (Area 7), then everything else.
What should you do if you cannot tick the blockers?
Do not launch until they are done, or launch to a private group with no real data. For the rest, write a dated list with an owner and fix it in the first week. If you want an expert to run this checklist against your app, our AI audit is a fixed-price option, and you can book a call to talk through your project. If you want to build the workflow yourself, see our Claude Code beginner guide, compare tools in Claude Code vs Cursor vs Codex, or join the Autopilot Studio course.
References and further reading
- OWASP Top 10:2025, the ten most critical web application risks.
- OWASP Top 10 for LLM Applications 2025, risks for apps that call language models.
- OWASP Cheat Sheet Series: password storage, SQL injection prevention and HTTP headers.
- The Twelve-Factor App: Config, keeping configuration out of code.
- Next.js: Production checklist, framework-level security, caching and performance checks.
- Supabase: Row Level Security, database-level access rules.
- Stripe: Webhooks, signature checks, retries and idempotency.
- Google SRE Book: Monitoring Distributed Systems, the four golden signals.
- web.dev: Web Vitals, Core Web Vitals thresholds.
- GitHub Docs: Secret scanning push protection and removing sensitive data.
- Veracode: 2025 GenAI Code Security Report, on the security of AI-written code.
