P0 issues mean the app can leak data or take fake payments. A Production Pass is the shortest path to a deploy you can actually charge for.
P0rls
RLS policy uses USING (true) — every row is readable
supabase/migrations/20240801000000_init.sql:21
· ~4.0h to fix
-- PLANTED ISSUE: USING (true) makes RLS a no-op
Why it matters. ENABLE ROW LEVEL SECURITY with USING (true) is theatre. The anon key can SELECT/UPDATE/DELETE everything. AI generators do this to 'make the demo work'.
Fix. Replace USING (true) with USING (auth.uid() = user_id) (or the real owner column). Split SELECT/INSERT/UPDATE/DELETE policies. Test with the anon key, not the service role.
Why it matters. Anyone with the repo (or a leaked GitHub Action log) gets live keys. Lovable/Bolt/v0 projects often commit .env because the generator treats it as source.
Fix. Rotate every key in this file. Add .env to .gitignore. Move secrets to Vercel/host env vars. Keep only VITE_/NEXT_PUBLIC_ values that are meant for the browser in a checked-in example file, with fake placeholders.
Why it matters. Anyone with the repo (or a leaked GitHub Action log) gets live keys. Lovable/Bolt/v0 projects often commit .env because the generator treats it as source.
Fix. Rotate every key in this file. Add .env to .gitignore. Move secrets to Vercel/host env vars. Keep only VITE_/NEXT_PUBLIC_ values that are meant for the browser in a checked-in example file, with fake placeholders.
P0secrets
window.env assignment looks like it dumps secrets into the page
public/window-env.js:1
· ~3.0h to fix
// PLANTED: another window.env dump
Why it matters. Anything on window is readable from DevTools.
Fix. Delete window.env. Use server env and a narrow public config object with anon/publishable keys only.
Why it matters. service_role bypasses Row Level Security. In a browser bundle it is public. This is the #1 way AI-generated Supabase apps leak every row.
Fix. Delete the key from any file under src/, app/, public/, or components/. Use the anon key plus RLS on the client. Put service_role only in a server route, Edge Function, or GitHub Action secret.
P0secrets
VITE_/NEXT_PUBLIC_ variable that looks like a private secret
Why it matters. sk_live / STRIPE_SECRET in the browser lets anyone issue refunds or create payouts as you.
Fix. Keep only the publishable key (pk_) in Vite/Next public env. Call Stripe from a server route. Rotate the secret if this file ever shipped.
P0secrets
Supabase service_role (or lookalike) in a client file
src/lib/supabase.js:3
· ~3.0h to fix
// PLANTED ISSUE: service_role used from the browser. FAKE key.
Why it matters. service_role bypasses Row Level Security. In a browser bundle it is public. This is the #1 way AI-generated Supabase apps leak every row.
Fix. Delete the key from any file under src/, app/, public/, or components/. Use the anon key plus RLS on the client. Put service_role only in a server route, Edge Function, or GitHub Action secret.
Why it matters. Vite inlines every VITE_ value into the browser bundle. Prefixing a service role or secret key with VITE_ does not hide it.
Fix. Rename and un-prefix. Only publishable keys (anon, pk_) belong in VITE_/NEXT_PUBLIC_. Everything else lives on the server.
P0stripe
Stripe webhook handler does not verify the signature
api/webhook.js:1
· ~2.5h to fix
// Intentionally broken Stripe webhook: trusts req.body, no constructEvent.
Why it matters. Without stripe.webhooks.constructEvent, anyone can POST a fake invoice.paid and mark themselves as paid. Lovable/Bolt apps often fetch('/api/webhook') from the client and trust the body.
Fix. Read the raw request body. Call stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET). Return 400 on failure. Never fetch the webhook from the browser.
P1oauth
Auth site URL / redirect is localhost
.env:10
· ~1.0h to fix
SITE_URL=http://localhost:5173
Why it matters. Production auth will bounce users back to your laptop.
Fix. Set SITE_URL and redirect allow-list to the production domain in the host's env, not in source.
P1oauth
OAuth redirect URI still points at localhost
src/lib/auth.js:4
· ~1.5h to fix
// PLANTED ISSUE: OAuth redirect stays on localhost
Why it matters. Google/GitHub/Supabase will refuse (or silently bounce) production logins if the only registered redirect is http://localhost:5173. This is the default the generator writes.
Fix. Add https://YOURDOMAIN/auth/callback (and the Vercel URL) to the provider and to Supabase Auth redirect URLs. Keep localhost for dev only. Never commit a production client secret next to it.
P1stripe
Client code calls the Stripe webhook URL
src/lib/stripe.js:6
· ~2.0h to fix
await fetch("/api/webhook", {
Why it matters. Webhooks are for Stripe's servers, not your React app. A client fetch skips signature checks and can be replayed.
Fix. Handle success in the checkout.session.completed webhook on the server. On the client, poll a server route that reads the order row.
P2observability
No Sentry (or other error tracker) found
(project):0
· ~1.5h to fix
No match for Sentry.init, @sentry/*, or sentry.io DSN in scanned files.
Why it matters. Vercel white screens and silent webhook failures are invisible without a client + server tracer. You will hear about them from Stripe or from a user, not from a log.
Fix. Add @sentry/browser (or @sentry/nextjs) with a DSN in server env. Capture unhandled rejections. Set tracesSampleRate low (0.1) until traffic is real.