Pre-launch checklist
The SaaS Production Readiness Checklist
For developers and indie founders whose prototype works and who are about to let real users pay. Run it the week before you flip billing on, not the week after a customer emails asking why they were charged twice. Every one of the 32 items below is something you can go do right now, not advice to file away. Clear all 21 critical items before launch, and treat the rest as a fast follow.
- Total checks
- 32
- Critical
- 21
- Recommended
- 11
Want the deep version of each item?
The checklist here is free to read and share. Enter your email and we’ll send the print-ready PDF plus a 6-part walkthrough that expands each section into the full production lesson. One lesson every few days, unsubscribe anytime.
Auth and account recovery
0/5The bugs here rarely show up in your own testing, because you never try to break into your own app or show up confused the way a stranger does.
- critical
- Why
- Any difference tells an attacker which emails belong to real accounts, a classic enumeration bug in auth code.
- How
- Open two tabs, submit both, diff the response and the network timing in devtools. They should match exactly.
- critical
- Why
- Without a limit, reset tokens can be brute forced, and the same flow can spam a user’s inbox as harassment.
- How
- Mash “forgot password” 20 times, confirm you’re throttled after a few, and confirm nothing crashes.
- critical
- Why
- If the other session survives, a stolen cookie outlives the exact action meant to kill it.
- How
- Log in on phone and laptop, reset from the laptop, refresh the phone, and confirm it’s logged out.
- critical
- Why
- Unmerged duplicate accounts split one person’s billing and history across two records.
- How
- Create the account by password, log out, sign up again via OAuth with the same email, and confirm you get one account.
- recommended
- Why
- Staging and production often use different OAuth client IDs and redirect URIs, and this exact mismatch breaks auth the moment it goes live.
- How
- From the deployed app, run a full OAuth login end to end and confirm the callback matches what’s registered with the provider.
Billing and webhooks
0/10This is where the real incidents cluster. Most of them are one missing idempotency check away from not happening.
- critical
- Why
- Stripe retries webhooks that time out or fail, and a non-idempotent handler turns that retry into a double charge or a duplicate order.
- How
- Run stripe listen, trigger charge.succeeded, then replay it from the Dashboard’s webhook log. Confirm your database shows one charge.
- critical
- Why
- A handler that writes a refund record on both the app action and the incoming webhook creates duplicate negative orders.
- How
- Refund a test charge in-app, check the orders table, trigger the webhook again, and confirm you still see one refund record.
- critical
- Why
- Without an idempotency key, a slow network or an impatient click turns one payment into two.
- How
- Throttle your network in devtools, click Pay twice fast, and confirm only one PaymentIntent exists for that order.
- critical
- Why
- A silently failing webhook means the payment succeeded but your app never found out, so the customer gets no access.
- How
- Point the endpoint at a dead URL, complete a test checkout, and confirm a reconciliation job or alert catches the mismatch, not silence.
- critical
- Why
- Test and live mode use different signing secrets, and the wrong one produces 500s on every webhook the moment you go live.
- How
- Compare STRIPE_WEBHOOK_SECRET against the Dashboard, send one live test event, and confirm a 200 response.
- critical
- Why
- A handler that logs invoice.payment_failed and does nothing leaves a delinquent account with full access indefinitely.
- How
- Trigger the decline, and confirm the account shows a payment-failed state and restricts access on your intended grace period.
- critical
- Why
- Cutting off access immediately instead of at period end is a support fire starter and a chargeback magnet.
- How
- Cancel a test subscription, check the access timestamp, and confirm it matches your stated policy, not the ORM default.
- recommended
- Why
- Stripe expects a fast 2xx, and a handler that takes too long gets treated as failed and retried, stacking duplicate processing on top.
- How
- Add the delay, trigger an event, check the Dashboard log for a timeout, then fix it by responding 200 immediately and queuing the real work.
- recommended
- Why
- Silent drift between the two is how unexplained subscription counts happen, and it erodes trust in your own revenue numbers.
- How
- Pull both counts by customer ID on a batch of test accounts, confirm zero mismatches, and schedule this as a real nightly job.
- recommended
- Why
- Test cards only exist in test mode, and SCA flows with real bank issuers can behave differently than the simulator, so the only true live check is a real card.
- How
- Run the 3DS challenge end to end in test mode first. Then, on production, buy your own product with a real card, confirm the redirect completes and access activates, and refund yourself from the Dashboard.
Admin and support readiness
0/2If you can’t fix a billing problem for a customer without opening a database console, you don’t have admin tools, you have a liability.
- critical
- Why
- Payment providers’ own refund flows do fail, and when that happens you need a way to make a customer whole without hand-editing rows under pressure.
- How
- Time yourself doing a full refund and cancel through your admin UI alone.
- recommended
- Why
- A slow admin lookup turns “why was I charged twice” into a multi-day back-and-forth that costs you the customer’s trust.
- How
- Pick a random test account and time how long it takes to pull their full history from the admin tool alone.
Onboarding and first-win
0/3Nobody outside the founding team has ever run the cold-start path. That’s the whole problem with this section.
- critical
- Why
- Products have launched to minimal signups and poor activation because nobody outside the founding team ever tried the cold-start path first.
- How
- Use a throwaway email, take no shortcuts, and write down every point of confusion.
- critical
- Why
- A mismatched sending domain or a missing plain-text version pushes transactional email into spam, so new users never see the email that gets them into the product.
- How
- Sign up with real inbox addresses, check the spam folder on each, and click every link in the email.
- recommended
- Why
- A dead-end “email already exists” error with no way to resume locks out a user who just got distracted.
- How
- Start signup, close the tab, retry with the same email, and confirm you get a clear path forward, not a wall.
Logging and observability
0/5If you can’t answer “what just happened” in under ten minutes, you’ll be answering it live, on a call, while a customer waits.
- critical
- Why
- Scattered logging turns a production incident into guesswork instead of a lookup, and recovery takes hours instead of minutes.
- How
- Pick a request ID, follow it through logs and your error tracker, and time it. Over 10 minutes means it isn’t launch-ready.
- critical
- Why
- Teams have found out about a provider outage only when angry customers emailed in, because nothing was watching the dependency itself.
- How
- Add a scheduled check per dependency, block the request in a firewall rule, and confirm you’re alerted before a customer would notice.
- recommended
- Why
- A service that hard-crashes because a queue is unreachable turns a minor outage into a full one.
- How
- Stop the dependency in staging, click through the app’s core flows, and confirm a degraded experience or a clear error, not a blank screen.
- recommended
- Why
- An unbounded retry loop against a metered API can burn through real budget for a handful of affected users before anyone notices, turning a bug into a bill.
- How
- Set a hard cap per request, trigger the failure in staging, and confirm the cap stops it and logs the abnormal usage.
- recommended
- Why
- Large changes shipped late in the week with no rehearsed rollback plan turn one bad deploy into a weekend-long fire.
- How
- In staging if you’re not confident doing it live, run the actual rollback command end to end and time it.
Security, rate limits, and backups
0/4A backup you haven’t restored isn’t a backup. Everything else here is a variant of that same problem.
- critical
- Why
- A backup that’s never been restore-tested is a backup in name only, and gaps surface during a real incident, too late to fix.
- How
- Restore your most recent backup into a throwaway environment, then check row counts and specific records against production.
- critical
- Why
- Secrets left hardcoded because they “just happened to work” on a dev machine break the first time you deploy to a new server.
- How
- Spin up a clean container, set only the vars in .env.example, and confirm the app boots and its core flows work.
- critical
- Why
- Without a rate limit, public form endpoints are a free target for credential stuffing and email-spam abuse. Webhook endpoints are the exception: throttling them drops your payment provider’s legitimate retries, so they’re protected by signature verification instead.
- How
- Write a quick loop hitting signup and reset, and confirm you get throttled with 429s partway through, not 100 successes or a crash. Then confirm your webhook handler verifies signatures on every request.
- recommended
- Why
- Without domain authentication, anyone can spoof your domain to phish your own customers, with no report showing you it’s happening.
- How
- Add a _dmarc TXT record starting with p=none and a report address, alongside SPF and DKIM, then confirm dmarc=pass in the headers.
Legal and compliance basics
0/3You don’t need a lawyer for these three. You do need to actually do them before you have paying customers whose data you’re responsible for.
- critical
- Why
- Without this inventory you can’t answer a regulator or a customer about what data exists, and “not sure” isn’t a legal basis.
- How
- List every personal-data field in your users table with its reason and retention period.
- critical
- Why
- A user left lingering in logs or a marketing tool after “deletion” means you haven’t actually honored the request.
- How
- Delete a test account through your own flow, then query every table referencing that user ID and confirm nothing personal remains.
- recommended
- Why
- Handling EU users’ data through a processor with no agreement in place is a basic compliance gap, cheap to fix now and expensive to discover later.
- How
- Go through your .env file’s API keys, and for each service that sees personal data, confirm a DPA exists on the vendor’s site.