12 min read

Pre-launch security checklist for vibe-coded apps (one hour, no security expertise required)

You're about to ship the app you built with Lovable, Bolt, v0, Cursor, or Replit. Before you press deploy, run this 60-minute checklist — 12 items that catch the bugs that actually leak data on launch day. Written for solo founders who don't want to learn security.

You're about to ship. The app works. You've tested the happy path. Your friends say it's cool.

Before you press deploy, run this checklist. 60 minutes, 12 items. Each item maps to a real incident that has happened to a real founder shipping a real AI-built app this year.

You can pretend the entire industry already learned these lessons; you can also just check the 12 items.

Database (15 min)

### 1. RLS on every Supabase table

Open Supabase → Authentication → Policies. Every table should show "RLS Enabled." If any table shows "RLS Disabled," that table is publicly readable by anyone with your anon key.

-- For each table missing RLS:
alter table public.your_table enable row level security;

Then add at least a default-deny policy or a per-user policy. Full guide.

### 2. Service-role key is server-side only

Search your repo for service_role and SUPABASE_SERVICE_ROLE_KEY:

grep -rn 'service_role\|SUPABASE_SERVICE_ROLE_KEY' .

For each match, ask: does this file run in the browser? In Next.js: anything in app/ or pages/ without "use server" at the top, anything in components/, anything in lib/ imported by either — runs in the browser. The service-role key cannot live there.

If you find one in client code: rotate immediately + move to server-side only.

### 3. JWT secret not exposed

In Supabase dashboard → Settings → API, your JWT secret must NOT be readable by the anon SQL role. Test:

-- As anon role:
select * from auth.config;

If this returns the JWT secret, you have a critical misconfig. Newer Supabase projects fix this by default; older projects may still be exposed. Revoke anon access.

Authorization (10 min)

### 4. Every API route with params.id checks ownership

For every route under app/api/[*]/[id]/route.ts that returns user-specific data, verify the handler compares the resource's owner against the authenticated user. Missing ownership check = BOLA / IDOR.

grep -rln 'params.id' app/api/

For each result, open the file and verify:

const { data: user } = await supabase.auth.getUser();
if (resource.user_id !== user.id) return new Response(null, { status: 404 });

Full pattern.

### 5. Middleware actually runs on the routes you think

Test:

curl -i https://your-app.com/api/admin/users
curl -i -H "x-middleware-subrequest: src/middleware" https://your-app.com/api/admin/users

The first must return 401/403/redirect, not the data. The second must also be blocked — if it's not, you have CVE-2025-29927 and you're on a vulnerable Next.js. Upgrade. Full write-up.

### 6. Admin routes require admin role, not just authenticated

Sign in as a regular user. Hit an admin route (/api/admin/*, /admin). If the response is the admin data instead of 404, your middleware checks for "logged in" but not for "is admin." Pattern fix.

Secrets (10 min)

### 7. No keys in git history

git log -p --all -S 'sk-' | head -50
git log -p --all -S 'AKIA' | head -50
git log -p --all -S 'ghp_' | head -50
git log -p --all -S 'eyJ' | head -200  # JWTs including Supabase service-role

Every match is a leak. Rotate the matched key immediately at the provider — don't bother removing the commit (history rewrites don't help once the key is in scrapers' pools). Full postmortem.

### 8. Spending limit on every API provider

For every paid API in your stack (OpenAI, Anthropic, Stripe, AWS), log into the provider and set a hard spending limit. Default limits are usually "no limit." A leaked key without a cap can become a $4,000 bill overnight.

  • OpenAI: dashboard → billing → limits → set hard limit to 5x your normal monthly spend
  • Anthropic: console → plans → spending limit
  • Stripe: dashboard → billing → set radar rules
  • AWS: Billing → budgets → max-spend cap (uses AWS Budgets + Service Quotas)

### 9. .env files are in .gitignore

cat .gitignore | grep -E '^\.env|env$'

Should match .env, .env.local, .env.production. If any are missing, add them now. Then check git history:

git log --all -- .env .env.local .env.production

If any of those have ever been committed, every secret in them is exposed. Rotate everything.

Webhooks (5 min)

### 10. Every webhook endpoint verifies signatures

For every webhook route (/api/webhooks/stripe, /api/webhooks/clerk, /api/webhooks/github), open the handler. The first thing it does should be verifying the request's signature against a shared secret:

// Stripe example:
const sig = req.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(
  await req.text(),
  sig,
  process.env.STRIPE_WEBHOOK_SECRET
);
// If constructEvent throws, the signature is invalid — reject.

If your webhook handler reads the body and does work without verifying the signature, an attacker can forge events (fake payments, fake user creation, fake auth events). Full guide.

Headers + transport (5 min)

### 11. Security headers are set

curl -I https://your-app.com/

You should see:

  • Strict-Transport-Security: max-age=31536000 — forces HTTPS for the next year
  • X-Frame-Options: DENY (or CSP equivalent) — defends against clickjacking
  • X-Content-Type-Options: nosniff — defends against MIME-sniff bugs
  • Content-Security-Policy — defends against XSS at the browser level

Vercel handles HSTS by default; the rest you configure in next.config.js headers function. Full pattern.

### 12. No HTTP anywhere

Every URL in your app should be HTTPS. Test by trying http://your-app.com (no s) — should redirect to https://. Any third-party API call your app makes should also be HTTPS — search for http:// in your code:

grep -rn 'http://' app/ lib/ pages/

Most matches will be comments / docstrings; legitimate http:// calls in 2026 are rare. Each match is worth a 30-second look.

Past the checklist

If you ran all 12 items and fixed what you found: ship. Watch your error rate carefully for 24 hours; a real attack often shows up as a spike in 401s + 404s as someone probes.

If the checklist took more than 60 minutes: that's the signal that you have material security debt. Two paths:

1. Sleep on it before launching. A 90-minute checklist run that found 4 issues means more issues are likely lurking. Defer the launch by 24 hours; do a deeper audit. The opportunity cost of one more day is real but small; the cost of launching with a bug that gets tweeted is unbounded.

2. Install [Securie](/signup) before you launch. The Day-1 specialists run the checklist's items 1-7 automatically on every PR — sandbox-verified, no false positives. Solo Founder tier ($49/mo) adds the Production-Readiness Certificate, a public verifiable URL listing the 50-control checklist evaluated against your codebase. Paste it into your launch announcement; that signals to procurement-conscious customers that you took the work seriously.

Stop the lock-step manual audit

The checklist above is a one-time launch gate. The right long-term answer is automation — every PR runs the same checks, every deploy is gated on the same surface, every commit produces a signed attestation.

Securie is built for that loop. Two minutes to install; zero engineering to maintain. The first scan finds the bugs the launch checklist missed; the recurring scans prevent regressions on every change.

Related

Related posts