AI builders make it possible to ship a real product in a weekend. They also make it possible to ship the same few security mistakes over and over, because the code works long before it's safe. This checklist covers the issues that matter most for a typical AI-built app: a React or Next.js frontend, a hosted database like Supabase or Firebase, login, and Stripe.
It's ordered by impact. If you only have an hour, do the first three sections.
1. Secrets
Leaked keys are the fastest way for a small app to get an unpleasant surprise, like an AI bill or a drained database.
- ☐ No secret keys in client code. Anything with a
NEXT_PUBLIC_,VITE_,REACT_APP_orEXPO_PUBLIC_prefix is shipped to the browser. Only publishable keys belong there. (Details) - ☐ No
.envfiles in git, now or in history. Check withgit log --all -- .env .env.local. - ☐ Any key that was ever exposed has been rotated at the provider. Deleting it from code is not enough.
- ☐ Spending limits and alerts are set on AI, email and SMS providers.
- ☐ Production and test keys are separate, and production uses live keys only where intended.
2. Database access
If your frontend talks to the database directly (as with Supabase or Firebase), the database rules are your backend security.
- ☐ Row Level Security is enabled on every table in Supabase's
publicschema, in your migrations, not just the dashboard. (RLS checklist) - ☐ Policies are scoped to the user, e.g.
using ((select auth.uid()) = user_id), with nousing (true)on personal data. - ☐ Update policies include
with check, so users can't reassign rows to someone else. - ☐ Users can't edit their own role, plan or credits. Keep privileged fields in a table users can't write to.
- ☐ Storage buckets holding private files are private, with per-user policies.
- ☐ Firebase users: Firestore and Storage rules don't use
allow read, write: if true, and test-mode rules have been replaced. - ☐ You've tested as a stranger: using only the public key, try to read another user's data. It should fail.
3. Authentication and authorization
- ☐ Every API route and server function checks the session before doing anything, and checks that the user owns the record they're asking for (not just that they're logged in).
// app/api/projects/[id]/route.ts
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session) return new Response("Unauthorized", { status: 401 });
const { id } = await params;
const project = await db.project.findFirst({ where: { id, ownerId: session.user.id } });
if (!project) return new Response("Not found", { status: 404 });
return Response.json(project);
}
- ☐ Admin pages are protected on the server, not just hidden in the UI.
- ☐ Middleware isn't your only check. Next.js middleware is useful for redirects, but authorize in the route or data layer too.
- ☐ Auth redirect URLs are restricted to your own domains in your auth provider's settings.
- ☐ Email confirmation is on if your app relies on users owning their email address.
- ☐ Password reset and magic links expire and work only once (default in most hosted auth providers; double-check custom implementations).
4. Payments
- ☐ Stripe webhooks verify the signature with
stripe.webhooks.constructEventusing the raw body. (Next.js guide) - ☐ Orders are fulfilled from the webhook, not from the success page.
- ☐ Prices come from the server (Stripe Price IDs), never from the request body.
- ☐ Webhook handling is idempotent, so a retried event doesn't grant access twice.
- ☐ The production webhook secret (
whsec_) matches the production endpoint, not the Stripe CLI's.
5. Input handling
- ☐ Validate request bodies on the server with a schema library such as Zod. Limit lengths and types.
- ☐ No raw SQL built from strings. Use parameterized queries or your ORM's query builder.
- ☐ No
dangerouslySetInnerHTMLwith user content unless it's sanitized (e.g. with DOMPurify). - ☐ File uploads check type and size, and are stored outside your web root or in a storage bucket.
- ☐ Server-side fetches of user-supplied URLs are restricted, to avoid your server being used to reach internal addresses.
6. Abuse and cost controls
- ☐ Rate limits on login, sign-up, password reset and any endpoint that calls a paid API.
- ☐ Bot protection (CAPTCHA or a turnstile-style check) on public forms if you see spam.
- ☐ Per-user quotas on AI features, so one account can't run up your bill.
7. Dependencies
- ☐
npm auditshows no critical or high vulnerabilities in production dependencies, or you know why each one doesn't apply. - ☐ Your framework is on a supported, patched version. Next.js, React and auth libraries publish security fixes regularly.
- ☐ Lockfile is committed, so production installs the versions you tested.
8. Headers and transport
- ☐ HTTPS everywhere, with HTTP redirected (most hosts do this by default).
- ☐ Security headers set where practical:
Strict-Transport-Security,X-Content-Type-Options: nosniff, aframe-ancestorsCSP directive orX-Frame-Options, and a sensibleReferrer-Policy. - ☐ CORS on your APIs allows only your own origins, not
*with credentials.
9. Privacy and data
- ☐ You know what personal data you store and why.
- ☐ Logs don't contain passwords, tokens or full card data.
- ☐ There's a privacy policy and a way for users to contact you about their data.
- ☐ Backups are on for your production database.
10. After launch
- ☐ Error monitoring (e.g. Sentry) so you see failures before users report them.
- ☐ Alerts for unusual spend on AI, email and hosting providers.
- ☐ Scans on every push, so new code doesn't reintroduce old problems.
What an automated scan can and can't do
Automated tools are good at the mechanical parts of this list: finding key patterns, committed .env files, tables created without RLS, API routes with no auth check, webhook handlers that skip signature verification, and dependencies with known CVEs. On our own deliberately vulnerable sample app (Next.js, Supabase, Stripe), PreflightPR found 16 issues, 12 critical, and its fix PR resolved 8 automatically, leaving a checklist for the human steps like key rotation.
They're not good at judging business logic, like whether user A should see project B. That part still needs you to think it through.
Run a free scan on a public repo to get your launch score and top issues in about a minute. It's an automated best-effort review, not a penetration test or security guarantee, so use it alongside this checklist, not instead of it.