# A pre-launch checklist for a vibe-coded app

> Eight checks before real users arrive: secrets, data access, validation, rate limits, backups, alerts, payment webhooks and environments.

Published: 2025-02-06 · Source: https://subxcat.com/articles/vibe-code-launch-checklist · Author: SUBXCAT

An app built in Lovable, Bolt, Replit or Cursor usually looks finished in a demo. Buttons work, data saves, the payment goes through. The trouble starts when real people show up, including a few who know exactly the wrong way to use it.

Here are the eight things we check first: what to look at, and how to tell it's broken. Most checks take minutes; fixes take an hour to a couple of days.

## Secrets in the client bundle

AI tools like to put a key wherever it works fastest, which often means code that ships to the browser. Anything in the bundle is public.

How to check: build for production and search the output for strings like `sk_`, `secret`, `service_role`, `api_key`. Open the Network tab and see whether the browser talks directly to OpenAI, Stripe or your database with a key in the headers.

How to tell it's broken: the client contains any key that grants more than read access to public data. The Supabase `anon` key in the client is expected. The `service_role` key is not; it bypasses every access rule you have.

## Access checks on the server

The most common hole: data is protected only by the UI. There's no button for "someone else's order", but a request to `/orders/123` with someone else's ID returns it anyway. In Supabase this looks like a table without Row Level Security, or with a policy of `using (true)`.

How to check: create two accounts. Make a record as the first, copy its ID, log in as the second and request it directly with curl or the browser console. Try to read it, then update it, then delete it.

How to tell it's broken: any of those requests succeeds. Separately, confirm RLS is enabled on every table, not just the ones you remembered.

## Input validation

The form won't let you enter a negative quantity, but the API accepts one. The name field is capped at 50 characters in the UI and at a megabyte on the server. The product price arrives from the browser and the server trusts it.

How to check: send requests that skip the form entirely, with empty fields, extra fields, huge strings, negative numbers and someone else's `user_id` in the body.

How to tell it's broken: the server stores what the form would have rejected, or takes from the request something it should compute itself, such as price, role or record owner.

## Rate limits

Without limits, one script can send a thousand password-reset emails or burn a month of AI budget overnight.

How to check: fire a hundred identical requests at login, signup, code sending and any endpoint that calls a paid API.

How to tell it's broken: all hundred succeed. You want limits per IP and per account at minimum, and tighter limits on the expensive operations.

## Backups and a restore drill

A backup you've never restored from is a hope, not a backup. Restores usually fail on small things: permissions, versions, uploaded files the database dump doesn't cover.

How to check: take the latest backup and restore it into a separate environment. Time it. Confirm it contains recent data and the uploaded files.

How to tell it's broken: you don't know where the backup lives or how long it's kept, or the restore didn't work on the first try. Better to find that out on a quiet Tuesday.

## Logs and error alerts

If you learn about errors from users, you hear about a fraction of them. Most people just leave.

How to check: break something on purpose in staging, say by throwing an exception in a handler, and see whether an alert arrives and how long it takes.

How to tell it's broken: no alert, or it drowns among a hundred others, or the log doesn't tell you which user and which request. Check the opposite too: passwords, tokens and full card numbers should never end up in logs.

## Idempotent payment webhooks

Payment providers resend webhooks, sometimes out of order, sometimes late. If every webhook grants a subscription or credits, a retry grants them twice.

How to check: send the same webhook twice (Stripe and similar providers have test tooling for this). Then send one with an invalid signature; it should be rejected.

How to tell it's broken: a double grant, or a webhook processed without a signature check. The minimum guard is to store the event ID and refuse to process it again:

```sql
create unique index on payment_events (provider_event_id);
```

## Separate environments

Test orders in the production database, real customers getting emails from staging, one API key for everything: the classic signs of a project that grew out of one environment.

How to check: development, staging and production need separate databases, separate payment keys (test and live) and separate email settings. Look at where the app reads its configuration and what happens when you run it locally.

How to tell it's broken: a local run writes to the production database, or can send a real email or charge a real card.

## What next

This list doesn't make an app secure. It removes the cheapest ways to break it. If three or more items come back "broken", push the launch by a week. That's cheaper than cleaning up afterwards.

SUBXCAT works through this list with your team, fixes what turns up, and leaves checks in place so it stays fixed.
