Security checklist
Vibe coding security checklist: what to check before you ship an AI-built app
AI app builders get you to a working app fast and leave the checks that keep it safe to you. Pick what your app is built on and get the ones that apply, each with how to verify it.
Updated . Every check links the vendor’s docs or OWASP.
32 checks for an app built on Next.js, Supabase, Stripe, Vercel and OpenAI: 7 must-dos, 16 should-dos and 9 good-to-haves.
Every AI-built app
Keep secret keys out of the browser: not in client code, not in public env vars
Must doAnything the browser downloads can be read, and AI builders often wire a secret key into client code to make a feature work. Only publishable keys belong in
NEXT_PUBLIC_,VITE_orEXPO_PUBLIC_variables.Verify from the command line
Build the app, then search the client output for secret key formats (Stripe, OpenAI, Anthropic, Supabase, AWS) and list your public variable names. A pass finds no keys, and no public variable is named like a secret.
grep -rnE "sk_live_|rk_live_|sk-(proj-)?[A-Za-z0-9_-]{20,}|sk-ant-|sb_secret_|AKIA[0-9A-Z]{16}" .next/static dist build 2>/dev/null grep -rhoE "(NEXT_PUBLIC|VITE|EXPO_PUBLIC)_[A-Z0-9_]+" --exclude-dir=node_modules . | sort -uCheck authorization on the server for every record, not only in the UI
Must doGenerated apps often hide a button but leave the API open, so anyone can fetch or change another user’s data by changing an id.
How to verify
Sign in as user A, copy a request for one of A’s records from the browser’s network tab, swap in the id of user B’s record and send it again. You should get 403 or 404, never B’s data. Repeat for update and delete requests.
Verify the signature on every webhook
Must doStripe, Clerk, GitHub and most others sign their webhooks. Without the check, anyone can post a fake “payment succeeded” or “user created” event to your endpoint.
Verify from the command line
Post to each webhook URL without a valid signature header; it must be rejected (400 or 401). For Stripe,
stripe listenandstripe triggersend real signed events to test the happy path.curl -i -X POST https://<your-app>/api/webhooks/<provider> -H "Content-Type: application/json" -d '{"type":"test"}'Rate-limit sign-in, sign-up, password reset and AI endpoints
Should doBots hammer login forms, and every call to an AI endpoint costs you money. One script can run up a month’s bill in an hour.
Verify from the command line
Send a burst of requests to your AI route and your sign-in route as one user or IP. After your limit you should get 429 responses.
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code} " -X POST https://<your-app>/api/<route> -H "Content-Type: application/json" -d '{"message":"hi"}'; doneValidate every input on the server with a schema
Should doClient-side checks are skipped by anyone calling your API directly; unvalidated input leads to injection bugs, crashes and junk data.
How to verify
Send each API route a request with a missing field, a wrong type and a very long string. Each should get a 400 with a short message, never a 500 or a saved record.
Treat AI model output as untrusted input
Should doText a user or a web page feeds the model can override your instructions. Never let model output run tools, queries or HTML without checks.
Check the code
Model output is escaped before it’s rendered (no dangerouslySetInnerHTML on raw output), and tool calls that change data need an allow-list or a user’s confirmation.
Allow only your own origins in CORS
Should doA wildcard origin with credentials lets any website call your API as your signed-in users.
Verify with a request
Send a request from an origin you don’t trust. A pass: no Access-Control-Allow-Origin for that origin, and never * together with credentials.
curl -sI https://<your-app>/api/<route> -H "Origin: https://evil.example" | grep -i access-control
Send security headers: Content-Security-Policy, HSTS and frame protection
Should doCheap protection against cross-site scripting, clickjacking and downgrade attacks.
Verify with a request
The site’s responses should carry these headers.
curl -sI https://<your-app> | grep -iE "content-security-policy|strict-transport-security|x-frame-options|x-content-type-options"
Check dependencies for known vulnerabilities before launch
Should doAI tools install packages from their training data, which can be versions with published vulnerabilities or packages that were deprecated since.
Verify from the command line
Run the free check at stackprism.ai/check on your repo, or npm audit for production dependencies. Fix anything critical before launch.
npm audit --omit=dev
Show generic error messages; keep stack traces out of responses
Good to haveStack traces and database errors tell an attacker your framework, file paths and table names.
How to verify
Request a page that doesn’t exist and send an API route malformed JSON in production. You should see a plain error page or a short JSON error, with no stack trace or SQL.
Log errors and sign-ins without secrets or personal data
Good to haveYou need logs to investigate an incident, but logs with tokens, passwords or card numbers become a leak of their own.
How to verify
Trigger a failed sign-in and a server error, then read the logs: the events are there, with no passwords, tokens, API keys or full request bodies.
Next.js
Full Next.js checklistCheck auth in every Server Action and Route Handler, not only in middleware
Must doMiddleware can be skipped by some routes and matchers; each data access needs its own check.
Verify from the command line
List every Server Action and Route Handler, then check each one reads the session and checks the user may touch that specific record before reading or writing data.
grep -rlE "['\"]use server['\"]" app src --include=*.ts --include=*.tsx find app src -name "route.ts" -o -name "route.js"
Add a Content Security Policy
Should doLimits the damage of any XSS bug.
Verify with a request
The response should carry a Content-Security-Policy header (a nonce-based one if you use inline scripts).
curl -sI https://<your-app> | grep -i content-security-policy
Keep data access in server-only modules
Good to haveStops server code and secrets leaking into client components by accident.
Check the code
Modules that touch the database or secrets start with import 'server-only'; importing one from a client component then fails the build.
import 'server-only';
Don’t render untrusted HTML with dangerouslySetInnerHTML
Should doIt skips React’s escaping, so user or AI-generated content can run scripts in your users’ browsers.
Verify from the command line
Every match should render trusted content or sanitize it first (for example with DOMPurify).
grep -rn "dangerouslySetInnerHTML" --include=*.tsx --include=*.jsx --exclude-dir=node_modules .
OpenAI
Full OpenAI checklistCall the API from your server only; never ship the key to clients
Must doA key in a browser or mobile bundle gets scraped and billed to you.
Verify from the command line
Build the app, then search the client output; a pass finds no key. No variable holding the key should start with NEXT_PUBLIC_ or VITE_.
grep -rnE "sk-[A-Za-z0-9_-]{20,}" .next/static dist build 2>/dev/nullSet project budgets and per-user rate limits
Should doOne abusive user or a retry loop can burn your monthly budget in hours.
How to verify
In the OpenAI dashboard, set a budget with an alert in your project’s limits settings. Then call your own AI route quickly several times as one user; your app should answer 429 after its per-user limit.
Retry 429s with exponential backoff
Good to haveRate limits are normal under load; fail gracefully instead of erroring.
Check the code
The official SDKs retry 429s and server errors twice by default; check you haven’t set maxRetries to 0 and that your UI shows a friendly message when retries run out.
const openai = new OpenAI({ maxRetries: 2, timeout: 60_000 });
Stripe
Full Stripe checklistSend idempotency keys on create requests
Should doA retried request after a timeout otherwise charges the customer twice.
Check the code
Every create call passes an idempotency key derived from your own record (order or cart id), so a retry can’t charge twice.
await stripe.paymentIntents.create(params, { idempotencyKey: `order-${order.id}` });Handle duplicate and out-of-order webhook events
Should doStripe delivers at least once; record processed event ids and don’t assume ordering.
Verify from the command line
Resend an event that was already processed. Your handler should recognise its id (stored in a processed-events table) and do nothing the second time.
stripe events resend <evt_id>
Use restricted API keys per service
Good to haveLimits the blast radius if one key leaks.
How to verify
In the Stripe Dashboard under Developers → API keys, each service should use its own restricted key (rk_live_…) with only the permissions it needs, and the full secret key shouldn’t be deployed where it isn’t needed.
Supabase
Full Supabase checklistEnable Row Level Security on every table
Must doTables in an exposed schema without RLS can be read and written by anyone holding the public anon key.
Verify with SQL
Run this in the SQL editor. It lists tables in the public schema with Row Level Security off; a pass returns no rows. The Security Advisor (Advisors in the dashboard) flags the same thing.
select tablename from pg_tables where schemaname = 'public' and not rowsecurity;
Keep the service_role key server-side only
Must doIt bypasses RLS. Never ship it to the browser, a mobile app or a NEXT_PUBLIC_ variable.
Verify from the command line
Search your code for public environment variables that hold a service role or secret key; a pass prints nothing. The key should only be read in server code (API routes, server actions, edge functions).
grep -rnE "(NEXT_PUBLIC|VITE|EXPO_PUBLIC)_[A-Z0-9_]*(SERVICE|SECRET)" --exclude-dir=node_modules .
Turn on Point-in-Time Recovery and test a restore
Should doDaily backups can lose up to a day of data; PITR restores to the second.
How to verify
Open Database → Backups in the dashboard. The Point in Time tab should show PITR switched on with a recovery window; then restore to a new project once to prove it works. PITR is an add-on on the Pro, Team and Enterprise plans.
Connect serverless functions through the connection pooler
Good to haveEach serverless invocation opening its own connection exhausts Postgres quickly under load.
Check the config
The connection string your serverless functions use should point at the pooler: a host ending in pooler.supabase.com on port 6543 (transaction mode), not the direct database host on 5432.
postgres://postgres.<project-ref>:<password>@<region>.pooler.supabase.com:6543/postgres
Walk through the Supabase production checklist
Good to haveCovers SSL enforcement, network restrictions, MFA on the org and email rate limits.
How to verify
Work through each section of the checklist, then open Advisors → Security Advisor and Performance Advisor in the dashboard: a pass shows no errors.
Authentication
Offer MFA, and require it for admin accounts
Should doStops most account takeovers from reused passwords.
How to verify
In your auth provider’s settings, MFA is available to users and required for admin accounts. Sign in as an admin from a new browser; it should ask for a second factor.
Set session expiry and revoke sessions on password change
Should doStolen sessions shouldn’t work forever.
How to verify
Sign in on two browsers, change the password in one, then refresh the other: it should be signed out. Sessions should also expire on their own.
Vercel
Full Vercel checklistProtect preview deployments
Should doPreview URLs are public by default and often talk to real data.
How to verify
Project → Settings → Deployment Protection: Vercel Authentication (or Password Protection) is on for preview deployments. Open a preview URL in a private window; it should ask you to sign in.
Scope environment variables per environment
Should doKeep production secrets out of Preview and Development builds.
Verify from the command line
List the project’s variables; production secrets should be scoped to Production only, not Preview or Development.
vercel env ls
Set spend management limits
Good to haveA traffic spike or a loop in a function shouldn’t produce a surprise bill.
How to verify
Team Settings → Billing → Spend Management: an on-demand budget is set with notifications, and Pause Production Deployments is on if you want a hard stop. (Pro and Enterprise plans.)
Review the Vercel Firewall and add rate limits to hot routes
Good to haveBlocks abusive traffic before it reaches your functions.
How to verify
Open the project’s Firewall tab: check the traffic view and that login, sign-up and AI routes have a rate limit rule.
Then check what your agent installed
AI tools pick package versions from their training data, which can be releases with published vulnerabilities. The dependency check reads your repo against today’s advisories, end-of-life dates and deprecations.
Questions
What is vibe coding, and why does it need its own checklist?
Vibe coding is building an app by describing it to an AI tool and accepting what it writes. The tools are good at making features work and bad at the parts you can’t see in the browser: who may read which record, where secret keys end up, and what happens when someone calls your API directly. This list covers those parts.
Is this list enough to call my app secure?
No. It catches the common, serious gaps in AI-built apps and tells you how to check each one yourself. If you handle payments at scale, health data or anything regulated, have someone review the app as well.
How do I check my dependencies?
Paste your public GitHub repo into the free dependency check. It reads your manifests and lists vulnerable, end-of-life and deprecated packages against live data, with a fix task you can give your coding agent.
Where do these checks come from?
From each vendor’s own documentation and the OWASP cheat sheets; every item links its source. The checks for individual technologies are StackPrism’s production checklists, which build on the lists the editor’s Improve panel uses.