Pre-launch checklist for vibe-coded apps

By BuildUnstuck · · checked against vendor docs on 2026-10-23 (links in each section). Docs change fast, so confirm the current wording before relying on a specific setting.

Who this is for: you built an app with an AI tool (Cursor, Claude Code, Codex, Lovable, Bolt, Replit, etc.) and it works in preview. This is the list to run before real users and real money show up.

How to use it: about 45–90 minutes. Do the sections in order. Tags: Blocker = don't launch without it. Should = fix this week. Nice = when you can.

What this is not: not a security audit or a guarantee. It catches the most common launch failures. If you'd rather not do it yourself, the last section says where we can help.

Ground rules

  • Do the risky tests on a staging copy (second project, test keys, fake data), not on your live app.
  • If a key was ever pasted into a chat, committed to git, or shipped to the browser, treat it as leaked and rotate it (§2, §5).
  • Don't give an AI agent your production credentials. Give it staging.

1. Auth and site URLs ("works in preview, broken live")

The classic launch failure: login works on the preview URL and fails on your real domain, or email links point to localhost.

Source: Supabase redirect URLs. The steps below are Supabase-specific; other auth providers (Clerk, Auth0, Firebase) have the same three ideas under different names.

  • Blocker Site URL is your production URL, not http://localhost:3000. Supabase: Authentication → URL Configuration. The docs call this setting critical for email confirmations and password resets.
  • Blocker Redirect URL allow-list contains your production URL(s). The redirectTo you pass in code must match an allowed entry.
  • Should In production, allow exact paths, not broad wildcards. Supabase says ** is useful for local development and preview URLs but recommends the exact redirect path for production. Keep preview-URL wildcards (e.g., Vercel preview patterns) separate and narrow.
  • Blocker OAuth providers (Google, GitHub, …) list the right callback. In the provider's console, the authorized redirect/callback URI must match what your auth provider tells you to use. Check it after you add your custom domain.
  • Blocker Email confirmation and password-reset links work end to end with a brand-new email address, in a private/incognito window, on the live domain. If your email template uses {{ .SiteURL }} but your code sets redirectTo, you may need {{ .RedirectTo }} (Supabase docs).
  • Blocker Real users can receive your emails. Supabase's default email provider only sends to members of your organization (changed 2024-09-26; see changelog). Configure a custom SMTP provider before launch, and on the Free plan, customizing auth email templates requires custom SMTP (changelog).
  • Should Set NEXT_PUBLIC_SITE_URL (or your framework's equivalent) to the production URL in the production environment, and build your redirect URL from it (Supabase's docs show a getURL() helper).
  • Should Don't await other Supabase calls inside the onAuthStateChange callback. Supabase's reference says the method is "safe to use without an async function as callback." Community reports of blank screens after login trace back to this pattern; if you see it, move the work out of the callback.
  • Should Sign out, sign in, refresh the page, open a second tab. Does the session survive? Does a protected page redirect when signed out?
  • Nice Add a test account and a "forgot password" run to your launch smoke test.

2. Environment variables

Source: Vercel env vars, Next.js env vars.

  • Blocker Write down every variable your app reads (search for process.env, import.meta.env, os.environ) and where each one is set: local, preview, production.
  • Blocker Production has its own values, set in your host's dashboard, not copied from .env. On Vercel, changes apply only to new deployments, so redeploy after changing a variable.
  • Blocker .env* files are in .gitignore (create-next-app does this by default; AI-generated projects may not). Only commit a .env.example with fake placeholder values.
  • Blocker Search git history for secrets. Run a scanner such as gitleaks detect or trufflehog git file://. on the repo. Anything found = rotate it; deleting the file doesn't remove it from history.
  • Blocker Know what the prefix does. In Next.js, anything named NEXT_PUBLIC_* is inlined into the JavaScript sent to every visitor. Vite-style VITE_* variables behave similarly. Only publishable/public values belong there.
  • Should Mark secrets as secret in your host (Vercel has Secret vs Config types; Secret values are write-only after saving).
  • Should Use different keys for preview, staging, and production (separate Stripe test/live keys, separate Supabase projects).
  • Should Don't print env vars in error pages, logs, or debug endpoints.
  • Should Keep a rotation list. If your host reports a breach (Railway told all users to rotate environment variables after its Oct 21, 2026 incident), you should be able to rotate everything in 30 minutes. Source: Railway disclosure.

3. Stripe webhooks (the "paid but nothing unlocked" and "anyone can fake a payment" section)

Source: Stripe webhooks docs, Stripe key best practices. A practitioner audit reported 26 of 30 vibe-coded MVPs without signature verification (self-reported), so this is the one to get right.

  • Blocker Your app unlocks access from a webhook event, not from the success redirect page. Anyone can visit /success. Use events like checkout.session.completed / payment_intent.succeeded (match to your flow).
  • Blocker You verify the Stripe-Signature header with the official library (Node: stripe.webhooks.constructEvent(rawBody, signatureHeader, endpointSecret)), and return 400 if verification fails. Without it, an attacker can POST fake events to your URL.
  • Blocker You pass the RAW request body to verification. Stripe requires the unmodified body; if your framework parses/re-serializes JSON first, verification fails (in Express, use express.raw({type: 'application/json'}) on that route; in Next.js route handlers, read the body as text). Tip: if every verification fails in production, this is the first suspect.
  • Blocker The signing secret (whsec_…) is set in production and is the right one. Each endpoint has its own secret, and test mode and live mode secrets differ. The secret printed by stripe listen (local testing) is also different from your deployed endpoint's secret.
  • Blocker You registered a live-mode webhook endpoint (HTTPS, publicly reachable) and subscribed only to the events you handle. Redirects (3xx) count as failures, so register the final URL.
  • Blocker Return a 2xx quickly, before slow work. Stripe retries failed deliveries in live mode for up to three days, so slow handlers create duplicate events.
  • Should Handle duplicate and out-of-order events. Store processed event IDs and skip repeats; don't assume event order; don't use created to dedupe.
  • Should Keep the default timestamp tolerance (5 minutes in Stripe's libraries). Never set tolerance to 0, which disables the recency check.
  • Should Secret key stays on the server; use a restricted key with only the permissions your server needs, not the all-powerful default. Never ship sk_live_… to the browser. (Vercel Connect added per-agent managed restricted Stripe keys on Oct 23, 2026, if you use Vercel: changelog.)
  • Should Test it: stripe listen --forward-to localhost:4242/webhook then stripe trigger payment_intent.succeeded locally; in a staging deployment, send a test event from the Dashboard; then send a request with a wrong signature and confirm you get a 400.
  • Should Roll the signing secret if it ever leaked. Stripe lets the old secret stay valid up to 24 hours so you can deploy the new one.
  • Nice Add an alert when webhook deliveries fail (Workbench → Webhooks → Event deliveries).

4. Supabase: grants and Row Level Security (RLS)

Source: Supabase RLS guide, API keys, Data API exposure change. Context: UpGuard reported 16,326 Supabase databases with publicly readable tables; it attributes this to missing/ineffective RLS and misused public keys (UpGuard).

Heads-up, date-sensitive: Supabase says that on Oct 30, 2026 the "don't auto-expose new tables to the Data API" setting is applied to all existing projects. Existing tables keep their current grants, but any new table in public will need an explicit GRANT before supabase-js can see it. If an AI tool creates your tables, it must include grants. The change affects Data API / supabase-js access, not direct Postgres connections.

  • Blocker RLS is enabled on every table in an exposed schema (usually public). Tables created through the API, SQL, or an AI agent don't necessarily have RLS on. Find the ones without it:
    select schemaname, tablename, rowsecurity
    from pg_tables
    where schemaname = 'public' and not rowsecurity;
    Fix: alter table public.your_table enable row level security;
  • Blocker Every table has policies for the operations it needs, and policies name the role. With RLS on and no policies, the API returns nothing, which is safe but broken. A permissive policy like using (true) for anon makes all rows public; use it only for truly public data.
    create policy "users read own rows"
      on public.your_table for select
      to authenticated
      using ((select auth.uid()) = user_id);
    Write separate policies for select, insert, update, delete. For update, include with check so users can't reassign rows to someone else.
  • Blocker Grants match intent. Postgres checks grants first, then RLS. On projects that still auto-grant, anon and authenticated can have every privilege by default, and adding policies doesn't remove those grants. Revoke and re-grant narrowly:
    revoke all on table public.your_table from anon, authenticated;
    grant select, insert, update, delete on table public.your_table to authenticated;
    (A missing grant returns error 42501; an empty result usually means a policy filtered the rows out. Check grants first when debugging.)
  • Blocker Never base a policy on user_metadata. Signed-in users can edit it. Use app_metadata or a table you control for roles/permissions.
  • Blocker The secret/service_role key is never in client code. Publishable (sb_publishable_…, or legacy anon) goes in the browser; secret (sb_secret_…, or legacy service_role) bypasses RLS and belongs only on servers. Supabase is deprecating the anon/service_role keys by the end of 2026. If a tool tells you to paste a long key starting eyJ, it's using legacy keys.
  • Should Views: a view over a protected table can bypass RLS. On Postgres 15+, create it with (security_invoker = true).
  • Should security definer functions: set search_path = '', qualify names, and keep them out of exposed schemas.
  • Should Test as a stranger. With only the publishable key and no login, try to read a table:
    curl "https://YOUR-PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*" \
      -H "apikey: YOUR_PUBLISHABLE_KEY"
    You should get an empty list or a permission error, never other people's rows. Then sign in as User A and try to read/update User B's row (should fail). Test on staging.
  • Should Run the Security Advisor (Dashboard) and read each finding before dismissing it. The Dashboard also has an RLS Tester in feature preview (as of the April 2026 changelog).
  • Should Add an index on columns your policies filter on (e.g., user_id) so reads stay fast as tables grow, and write (select auth.uid()) instead of bare auth.uid().
  • Nice Add supabase test db pgTAP tests for allow/deny per role.
  • Should Storage buckets have policies too. Check that private files aren't readable via public URLs.

5. API key and secret exposure (the "surprise $4,200 bill" section)

Source: Cyble, Oct 22, 2026: of 22,306 AI-built sites it identified, 35 shipped a genuinely private secret to the browser (OpenAI keys, Stripe secret keys, Supabase service-role tokens, AWS keys, a Slack webhook shared across a 13-domain fleet).

  • Blocker Search your built code for secrets. Build for production, then search the output (e.g., .next/static, dist, build) and view-source in the browser:
    grep -rEn "sk-|sk_live_|service_role|sb_secret_|hooks\.slack\.com|AKIA[0-9A-Z]{16}" .next/static dist build 2>/dev/null
    (sk_test_ keys can be sensitive too. Publishable/anon keys are expected in the browser. Ignore false positives, but investigate every hit.)
  • Blocker LLM calls go through your server, not the browser. Put the model provider key in a server route/edge function that checks the user is signed in. A key in the front end is a key on every visitor's screen.
  • Blocker Rotate anything that ever touched the client or git history — rotate first, investigate second. Supabase has a documented rotation procedure: create the new secret key, swap it everywhere, confirm, then delete the old one.
  • Blocker Every AI-powered endpoint requires auth AND has limits: per-user rate limiting, max tokens/request size, and a budget/usage cap set in the provider's console. (In the same practitioner audit, 28 of 30 apps had no rate limiting; self-reported.)
  • Should Restrict third-party keys to the minimum (Stripe restricted keys; Google API keys restricted to your domains/APIs; provider keys limited by project where supported).
  • Should Turn on secret scanning (GitHub secret scanning/push protection if available on your plan, or a pre-commit hook such as gitleaks).
  • Should Check dependencies before you install. On Oct 21, 2026, the litellm==1.107.0 package on PyPI was found to contain credential-stealing malware (live about two hours; LiteLLM says earlier versions are safe). Pin dependency versions and commit your lockfile, and review packages an AI tool adds. Source: LiteLLM.
  • Should Give coding agents the least they need: no production keys in the project folder, .env not readable by the agent if your tool supports deny rules, and review MCP servers before connecting them (a recently reported OpenCode flaw let a malicious MCP server run code on Windows/WSL during sign-in; fixed in 1.18.33 per The Hacker News).
  • Nice Set up billing/usage alerts on every paid API and your host.

6. Backups, staging, and "don't let the agent touch prod"

Source: Supabase backups, Business Insider on the Replit incident.

  • Blocker You know what your backups are, and you've tested a restore. Supabase: daily backups on Pro (7 days), Team (14), Enterprise (up to 30); Free-tier projects should export with the CLI (supabase db dump) and keep copies off-site. Point-in-Time Recovery is a paid add-on. Backups don't include files stored through Supabase Storage. Deleting a project permanently deletes its backups.
  • Blocker You have a separate staging project with its own database, keys, and Stripe test mode. Run migrations and AI-generated changes there first.
  • Blocker No coding agent has production database credentials. Reported in July 2025: an AI agent deleted a company's production database during a code freeze (Replit/SaaStr). Use staging credentials for agents; do production changes yourself, reviewed.
  • Should Schema changes live in migration files in git, not only in click-ops or one-off agent SQL, so you can recreate the database.
  • Should Know your rollback. On your host, find the "redeploy previous version" button before you need it.
  • Should Add basic error visibility: an error boundary in the UI, server logs you can read, and an error-tracking tool or at least an uptime check.
  • Nice Turn on branch protection and require a human review for changes to auth, payments, and database policies.

7. Final 10-minute smoke test (on the live URL)

  1. Sign up with a new email in an incognito window; confirm the email arrives and the link goes to your real domain.
  2. Sign out, sign in, refresh, open a second tab.
  3. Try to view another user's data by changing an ID in the URL/API call (should fail).
  4. Buy something with a Stripe test card on staging; confirm the webhook arrives, signature verifies, and the feature unlocks exactly once.
  5. Send a bad-signature request to your webhook (should be 400).
  6. View source / search the bundle for secrets (§5).
  7. Hit your AI endpoint 50 times quickly while signed out and signed in; confirm it blocks or limits.
  8. Confirm a backup exists and you know how to restore it.
  9. Check logs for errors from steps 1–7.

Need a hand?

If something above failed and you're stuck in a fix-and-break loop, BuildUnstuck does fixed-price fixes: $149 single fix, $399 deploy + auth + payments, $799 integration/security pass (founding rates of $109, $299 and $599 apply to the first 10 jobs). Scope and exclusions are written down before you pay. Details: pricing and what is in scope, or tell us what is broken.

Get one practical fix in your inbox each weekday

The free BuildUnstuck newsletter covers what shipped, what broke, and one fix you can make today. Every item links to its source. Free, unsubscribe any time.

By subscribing you agree to receive the newsletter. See the terms (draft) and privacy page (draft).

This checklist is general information, not professional security, legal, or financial advice, and it does not guarantee that an app is secure.