The 10-minute security check before you launch a vibe-coded app (Supabase + Stripe) A developer published a 10-minute pre-launch security checklist for apps built with AI coding agents such as Cursor, Claude Code, Lovable and Bolt, targeting common Supabase and Stripe misconfigurations. The checks include curl probes to confirm row-level security is enabled on every table, grepping built bundles for leaked OpenAI, Anthropic, Stripe and Supabase service_role keys, scanning git history for committed secrets, and verifying Stripe payments through signed webhooks using the raw request body rather than a success-page URL. I build businesses that solve the technical problems people run into, and this one keeps coming up. AI coding agents are great at "make it work" and bad at "make sure nobody else can." If you built your app with Cursor, Claude Code, Lovable or Bolt, the same handful of holes show up again and again: tables anyone can read, secret keys in the frontend, and payment flows you can unlock with curl. None of these need a security background to find. Here are seven checks you can run in about ten minutes, against your live app, before you post that launch tweet. Most take one command. Your Supabase anon key is public by design. It's in your frontend bundle, and anyone can copy it. That's fine only if row-level security RLS is on for every table and the policies are real. So test what a stranger sees. Grab your project URL and anon key from the browser's Network tab, then: curl "https://YOUR PROJECT.supabase.co/rest/v1/profiles?select= &limit=5" \ -H "apikey: YOUR ANON KEY" \ -H "Authorization: Bearer YOUR ANON KEY" Repeat for every table. If rows come back while you're logged out, either RLS is off or a policy says using true . To see every table at once, run this read-only query in the Supabase SQL editor: select c.relname as table name, c.relrowsecurity as rls enabled from pg class c join pg namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind = 'r' order by rls enabled, table name; Any false is a table anyone can read and write. The usual fix is an owner-only policy: alter table public.notes enable row level security; create policy "owner can read" on public.notes for select using auth.uid = user id ; Add matching insert/update/delete policies, using with check on insert and update. RLS stops strangers. The next bug is logged-in users reading each other's data, often through your own API routes, which may use a service key and skip RLS entirely. If B's data comes back, your API checks login but not ownership . Every read, update and delete needs a "does this row belong to this user?" check on the server. Hiding a button in the UI doesn't count. Anything prefixed NEXT PUBLIC , VITE , EXPO PUBLIC or REACT APP is copied into the bundle every visitor downloads. Agents love to "fix" a missing-key error by adding that prefix. First, check your env files for secret-sounding public variables: grep -E "^ NEXT PUBLIC |VITE |EXPO PUBLIC |REACT APP A-Z SECRET|SERVICE|PRIVATE|OPENAI|ANTHROPIC " .env Then build and search the output dist , build or .next/static for real key shapes: npm run build grep -rEo "sk live A-Za-z0-9 +|sk-proj- A-Za-z0-9 - +|sk-ant- A-Za-z0-9 - +|sb secret A-Za-z0-9 - +" dist .next/static 2 /dev/null The Supabase service role key is a JWT, so the word "service role" usually isn't visible in plain text. Find any JWTs in the bundle and decode the middle part: grep -rEoh "eyJ A-Za-z0-9 - +\.eyJ A-Za-z0-9 - +" dist .next/static 2 /dev/null | sort -u \ | while read t; do echo "$t" | cut -d. -f2 | base64 -d 2 /dev/null; echo; done "role":"anon" is fine. "role":"service role" means anyone can bypass all of your RLS. Move it to server-only code and rotate it. Also check git history, because deleting a file doesn't delete the commit: git log -p | grep -E "sk live|sk-ant|sk-proj|service role" If anything turns up, rotate the key at the provider . Removing it from the code revokes nothing. Two common payment bugs: /success?session id=... is just a URL. Anyone can type it. Grant access only from a verified webhook. checkout.session.completed event. Signature checks need the raw request body. A parsed-then-re-stringified JSON body will fail verification. In a Next.js route handler: js export async function POST req: Request { const body = await req.text ; // raw body, not req.json const sig = req.headers.get "stripe-signature" ?? ""; let event; try { event = stripe.webhooks.constructEvent body, sig, process.env.STRIPE WEBHOOK SECRET ; } catch { return new Response "Invalid signature", { status: 400 } ; } // Idempotency: Stripe retries, so the same event can arrive twice. // Skip event.id values you've already processed store them in a table . if event.type === "checkout.session.completed" { // grant access here, looked up from the session, not from the browser } return new Response "ok" ; } Test it: should return 400, not 200 curl -i -X POST https://yourapp.com/api/stripe/webhook -d '{"type":"checkout.session.completed"}' Then pay in test mode and close the tab before the redirect. Access should still unlock. Resend the same event from the Stripe dashboard and confirm nothing gets granted twice. Remember that test and live webhook endpoints have different whsec secrets. While you're there, make sure prices come from the server a price ID you look up , not from an amount the browser sends. Static hosts serve whatever ends up in the output folder. A stray .env or .git folder there is public. for p in .env .env.local .env.production .git/config .git/HEAD .DS Store; do printf "%-18s" "$p"; curl -s -o /dev/null -w "%{http code}\n" "https://yourapp.com/$p" done Each should be 404 . If you get 200 , open the URL. SPAs often return index.html for every path, which is harmless. Real file contents are not. An LLM route with no auth and no limit is a blank check. One script running overnight can produce a very large bill. max tokens and an input length limit. Table RLS doesn't cover files. Supabase Storage uses policies on storage.objects , and a bucket marked public serves every file to anyone with the URL. Check each bucket's public toggle, then add a policy for private ones, for example "users read only their own folder": create policy "own files" on storage.objects for select using bucket id = 'uploads' and auth.uid ::text = storage.foldername name 1 ; Test it by opening a file URL from a private bucket in an incognito window. It should fail. I got tired of running these checks by hand, so I wrote a small scanner for the code-level ones, and it's free. It's one file with zero dependencies Node 18+ , it runs locally, and nothing gets uploaded. It looks for keys in source and git history, secret-looking public env vars, tables created without RLS, using true policies, unverified Stripe webhooks and client-side LLM calls. It writes a report with a fix prompt you can paste into your coding agent, and it exits non-zero on critical findings so it can run in CI. Read the source first, then: curl -O https://preflight-kit.pages.dev/preflight.mjs node preflight.mjs A scanner can't see your dashboard settings or your logic, though. That's why the manual checks above logged-out queries, the A/B replay, the webhook test still matter. The scanner is part of something I built called PreFlight Kit. The paid kit $19 adds 12 copy-paste audit prompts for your coding agent IDOR, RLS, never-trust-the-client, payments, AI cost caps, prompt injection and more , an AGENTS.md / CLAUDE.md security section plus a Cursor rule so your agent stops reintroducing these bugs, a Supabase RLS audit query and policy templates, a 30-point launch checklist, and an "I leaked a key" runbook. If that's useful, code LAUNCH10 takes it to $9 through Sunday, Nov 1: preflight-kit.pages.dev https://preflight-kit.pages.dev . If not, the seven checks above cover the most common holes. Run them before you launch. Disclosure: I make PreFlight Kit.