The pre-launch checklist
Shipping isn't a button you press — it's the moment your app stops being forgiving. Locally, you are the only user, you never abuse your own forms, and every secret sits safely in your head. The pre-launch checklist is the discipline of imagining a hostile, careless, curious stranger using the thing you built, and closing the gaps before they find them.
Lock down secrets before anything else — this is the one that ends companies
The single most common way a vibe-coded app dies is a leaked API key. AI assistants happily hardcode keys inline, commit .env files, or expose server secrets to the browser. Do three things. First, run a real scan: `git log -p | grep -iE 'sk-|key|secret|token'` and check GitHub's Secret Scanning tab — if a key ever touched a commit, it is compromised forever, so rotate it, don't just delete the line. Second, understand the client/server boundary: in Next.js anything prefixed NEXT_PUBLIC_ ships to the browser, so your Stripe secret key, database URL, and Resend key must NEVER carry that prefix. Third, set spend limits on every provider dashboard (OpenAI, Anthropic, Stripe) so a leaked key or a runaway loop can't generate a five-figure bill overnight.
# Prove no secrets are tracked before you push public
git rm --cached .env .env.local 2>/dev/null
echo -e ".env\n.env*.local" >> .gitignore
# Then rotate every key that was ever committed — deletion is not enoughAssume every input is an attack, because it is
Your happy-path testing lied to you. Real users paste 40,000-word essays into a name field, submit forms with the network throttled, and send `'; DROP TABLE` out of boredom. Validate on the server, always — client-side validation is a UX nicety, not a security control, because anyone can hit your API directly with curl. Use a schema validator like Zod at the edge of every API route and parse the request before you trust a single field. If you use an ORM like Drizzle or Prisma with parameterized queries you're largely safe from SQL injection, but the moment you string-concatenate user input into a query or an LLM prompt that has database access, you've reopened the door. Rate-limit anything expensive — auth endpoints, AI calls, email sends — or someone will script it.
const Body = z.object({
email: z.string().email(),
message: z.string().min(1).max(2000),
});
export async function POST(req: Request) {
const parsed = Body.safeParse(await req.json());
if (!parsed.success) return Response.json({ error: "Invalid" }, { status: 400 });
// only now do you touch the database
}Auth is not 'is there a token' — it's 'is THIS user allowed to touch THIS row'
Authentication (who are you) is the easy half, and tools like Clerk or Supabase Auth hand it to you. The half that gets vibe coders breached is authorization — checking ownership on every single query. The classic bug: `/api/orders/[id]` fetches the order by ID and returns it, without checking that the logged-in user actually owns that order. That's an IDOR vulnerability, and it means user 41 can read user 42's data just by changing a number in the URL. Every database read and write that touches user-owned data must filter by the authenticated user's ID, not just by the resource ID. If you're on Supabase, turn on Row Level Security — an unprotected Postgres table behind their auto-generated API is world-readable, and people scan for exactly this.
// WRONG — returns anyone's order
const order = await db.query.orders.findFirst({ where: eq(orders.id, id) });
// RIGHT — scoped to the caller
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, id), eq(orders.userId, session.user.id)),
});Errors will happen — decide now whether users see a crash or a shrug
In development a thrown exception shows you a helpful stack trace. In production that same trace can leak your file paths, query structure, and env details to an attacker — or just show your user a white screen of death. Wrap your app in error boundaries so a single broken component degrades gracefully instead of blanking the page. Return generic error messages to clients (`Something went wrong`) while logging the real detail server-side. Most importantly, install error tracking BEFORE launch, not after your first outage — Sentry takes ten minutes and is the difference between 'a user emailed to say it's broken' and 'I got paged with the exact stack trace three seconds after it happened.' You cannot fix what you cannot see, and users almost never report bugs; they just leave.
Test the paths that touch money and data, even if you test nothing else
You don't need 100% coverage to launch. You need to sleep at night knowing the four or five flows that would be catastrophic if broken actually work: sign up, log in, the core action your app exists to do, and — if you charge — the payment flow. Write a handful of Playwright end-to-end tests that drive a real browser through those journeys, because they catch the integration failures unit tests miss: the redirect that loops, the webhook that never fires, the button that's disabled on mobile. For Stripe specifically, test with their test-mode cards AND verify your webhook handler is idempotent — Stripe retries webhooks, and if your 'mark as paid' logic runs twice you'll double-fulfill or double-credit. Trigger a real webhook with the Stripe CLI before you trust it.
# Replay real webhook events at your local handler before launch
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completedThe boring production settings that separate a demo from a product
A pile of small things quietly break the first time your app leaves localhost. Set every environment variable in your host (Vercel, Railway, Fly) — the app that works locally will crash on deploy because you never added DATABASE_URL to the dashboard. Configure your custom domain and confirm HTTPS is enforced. Set real CORS rules instead of the wildcard `*` you used while flailing. Add security headers (a Content-Security-Policy, HSTS) — Next.js lets you do this in next.config. Configure your email sender's SPF and DKIM DNS records or every transactional email from Resend lands in spam, which silently kills your password resets and receipts. And run a production build locally (`npm run build`) before you deploy — TypeScript errors and missing imports that your dev server tolerated will fail the build, and you'd rather find that on your machine than in a deploy log.
Have a rollback plan and watch the first hour like a hawk
You will ship a bug. The mature question isn't 'how do I avoid that' — it's 'how fast can I undo it.' Deploy from a git branch through pull requests so every release is a known commit you can revert to with one click; platforms like Vercel keep every previous deployment and let you instantly promote an old one back to production. Before you announce anything, do a full smoke test on the real production URL in an incognito window — not localhost, the actual deployed thing — because production has different env vars, a different database, and a cold cache. Then don't walk away. The first hour after launch is when you learn what your testing missed, so keep your error tracker and your host's logs open. Ship to a few real users first if you can; a quiet soft launch beats a loud broken one.
Cover the legal and trust basics so launch day isn't a liability
This isn't glamorous but it's cheap insurance. If you collect any personal data — even just emails — you need a privacy policy and terms of service; generators exist and a basic one takes twenty minutes. If you have EU users you're in GDPR territory, which at minimum means a cookie consent banner for non-essential tracking and a real way for users to delete their account and data. Never store passwords yourself — that's exactly why you offload auth to Clerk or Supabase, who hash them properly. Make sure your database has automated backups turned on (Supabase and most managed Postgres hosts do this, but confirm it) and that you've actually tested a restore once, because a backup you've never restored is a hope, not a backup. Add a way for users to reach you — even a plain support email — because the alternative to a bug report is a chargeback or a bad tweet.
Takeaways
- →Rotate every key that ever touched a commit — deleting the line does nothing, and set spend caps on every paid API
- →Validate and authorize on the server for every request; client checks are UX, not security
- →The breach that gets vibe coders is IDOR — filter every query by the authenticated user's ID, not just the resource ID
- →Install Sentry and test the money/auth flows with Playwright before launch, not after your first outage
- →Run `npm run build` and smoke-test the real production URL in incognito before you announce anything
- →Deploy through git so rollback is one click, and watch logs live for the first hour