Free checklist from Ship Safe
25 checks you can run before real users and real money arrive. Plain words, no jargon, a fix for every one.
You built an app with Lovable, Bolt, Cursor, Claude Code or Codex, and it works. Before you launch, you want to know what an attacker, a bot or a busy month could do to it.
Read the checks below for free. Each one says what to look for, why it matters and how to fix it. To keep a copy, print this page: it prints cleanly, without the menu.
Read it online at menelabs.com/ShipSafe/checklist. Book a Safety Audit at menelabs.com/ShipSafe.
Go through the checks in order. Most take a few minutes. If you can't answer one, that is your answer: find out before you launch. You can also paste a check into your AI tool and ask it to look for the problem in your code, then read what it changes before you accept it.
The groups follow the order things usually go wrong: leaked keys first, surprise bills last.
Look for: open your live app, right-click, choose "View page source" or open the browser's developer tools, and search for key, secret, sk_ and service_role.
Why it matters: anything the browser downloads, anyone can read. A secret key there lets a stranger use your database, your payment account or your AI credits as if they were you.
How to fix: keep secret keys on the server only (a server function, an API route, or your platform's edge functions). Publishable keys, like a Supabase anon key or a Stripe publishable key, are meant to be public, but only when the rules in the database section are in place.
Look for: search your repository, including old commits, for .env files and key-shaped strings. GitHub's secret scanning shows alerts on the repository's Security tab.
Why it matters: deleting a key from your code doesn't delete it from the history. A public repository with an old key in it is a leaked key.
How to fix: treat any key that was ever committed as leaked. Create a new one, switch your app to it, delete the old one, then add .env to .gitignore.
Look for: a list of every service your app uses (database, payments, email, AI, hosting) and where each key is stored.
Why it matters: when a key leaks, the damage depends on how fast you can replace it. Hunting through code at midnight is slow.
How to fix: keep keys in your hosting platform's environment variables, never in the code, and write down where each one lives. Practise replacing one.
Look for: try opening yourapp.com/.env, yourapp.com/.git/config and yourapp.com/config.json in a browser.
Why it matters: some setups publish every file in the project folder, settings files included.
How to fix: if any of those opens, move the file out of the public folder and change every key it contained.
Look for: pages or buttons hidden from normal users, like "Delete" or "Admin". Ask: if someone called that action directly, would the server refuse?
Why it matters: hiding a button only hides it. AI tools often protect the screen and forget the server behind it.
How to fix: every server function and API route checks that the user is signed in and allowed to do this action, before it does anything.
Look for: sign in as one user, open something that belongs to you (an order, a profile, an invoice), and change the ID in the address bar or the request to someone else's.
Why it matters: this is the most common leak in web apps. One changed number in a link shows another customer's data.
How to fix: every query that reads or changes data filters by the signed-in user, on the server or in the database rules, never just in the browser.
Look for: how your app decides someone is an admin. A field the user can edit, a value stored in the browser, or a hidden page with no check is not enough.
Why it matters: an admin flag a user can set on themselves gives them everything.
How to fix: store roles where users can't change them (a table only the server writes to, or your auth provider's role settings), and check the role on the server.
Look for: what happens after 20 wrong passwords in a minute, and how long password-reset links stay valid.
Why it matters: without limits, bots try passwords until one works and flood your inbox with reset emails.
How to fix: turn on rate limits and bot protection in your auth provider, keep reset links short-lived and single-use, and offer sign-in with Google or a magic link if passwords aren't essential.
Look for: in Supabase, open each table. A table with row level security (RLS) turned off can be read and changed by anyone who has your public anon key, and that key is in your app.
Why it matters: this is how most Supabase-based apps leak: the anon key is public by design, and RLS is what stops it from opening the whole database.
How to fix: turn on RLS for every table, then add policies that allow only what each user should do, such as "users can read and update their own rows".
Look for: a test that signs in as user A and tries to read user B's rows, and one that tries with no sign-in at all.
Why it matters: a policy that looks right can still allow too much. One wrong condition opens a table.
How to fix: write those two tests and run them whenever the database changes. Your AI tool can write them if you describe exactly what each user may see.
Look for: in Firebase, open the rules for Firestore, Realtime Database and Storage. Rules that allow everything, or allow everything until a date, are test mode.
Why it matters: test mode lets anyone read and write your whole database. It is meant for the first days of a project, not for launch.
How to fix: replace them with rules that check request.auth and allow each user only their own documents. Use the Rules Playground to try them as different users.
Look for: storage buckets (Supabase Storage, Firebase Storage, S3) marked public, and file links that work in a private browser window with no sign-in.
Why it matters: invoices, ID photos and private documents in a public bucket can be found and downloaded by anyone.
How to fix: make buckets private, serve private files through short-lived signed links, and keep public buckets for things like logos and product photos only.
Look for: where your checkout gets the price and the total. If the browser sends the amount, a user can change it.
Why it matters: someone edits one number in the request and pays 1 € for a 100 € order.
How to fix: the browser sends only what was chosen (product and quantity). The server looks up the prices and creates the payment.
Look for: the code that marks an order as paid when Stripe, Paddle or another provider calls your app. Does it check the webhook's signature?
Why it matters: without a signature check, anyone can call that address and mark orders as paid.
How to fix: verify every webhook with the provider's signing secret, and confirm the payment with the provider before you deliver anything.
Look for: text fields with no length limit, file uploads with no size or type limit, and data saved without checking.
Why it matters: oversized uploads fill your storage and your bill. Unchecked input can break pages or inject code that runs in other users' browsers.
How to fix: check every input on the server: type, length and allowed values. Limit file size and type on upload.
Look for: sign-up, contact and comment forms with nothing stopping automated posts.
Why it matters: bots create fake accounts, fill your database with spam and send email from your domain, which can get it blocked.
How to fix: add a honeypot field and a CAPTCHA such as Cloudflare Turnstile, and rate-limit how often one visitor can submit.
Look for: the billing settings of your hosting, database, email and storage providers. Is there a hard cap, or at least an alert well before your limit?
Why it matters: a loop that never stops, a viral day or a bot attack can run up a month's bill in hours, and you find out when the invoice arrives.
How to fix: turn on spend caps where they exist (Supabase and Vercel offer them), and budget alerts where they don't (Firebase on the Blaze plan and most cloud providers only alert). Set them low enough to wake you up.
Look for: the usage limits on your OpenAI, Anthropic, Google or other AI accounts, and whether one key is shared by everything.
Why it matters: an AI feature anyone can call without limits is a free AI service for strangers, paid by you.
How to fix: set a monthly spend limit in each AI provider's console, use a separate key per app, and limit how many AI requests one user can make per minute and per day.
Look for: API routes and server functions that anyone can call without signing in: search, sign-up, "send me a link", anything that sends email or calls an AI model.
Why it matters: each call costs you something. A script calling them thousands of times costs you thousands of times.
How to fix: add rate limits per IP address and per user, through your platform's firewall or a small rate-limit check in the code.
Look for: whether your database has automatic backups, how far back they go, and whether anyone has ever restored one.
Why it matters: a bad migration, a wrong delete or an AI change that wipes a table happens to everyone once. A backup you have never restored is a hope, not a backup.
How to fix: turn on daily backups (point-in-time recovery if your plan has it), then restore one into a test project and check the data is there.
Look for: where your app's errors end up. If the answer is "the browser console" or "nowhere", you are flying blind.
Why it matters: users rarely report errors. They leave.
How to fix: send server and browser errors to an error tracker or your platform's logs, and get an alert when the error rate jumps.
Look for: what would tell you the app is down at 3 a.m., other than an angry customer.
Why it matters: every hour of downtime costs sign-ups and trust, and a down app often means something worse happened.
How to fix: set up a free uptime monitor that checks your main page and one real feature every few minutes and messages you when it fails.
Look for: an AGENTS.md, CLAUDE.md or rules file in the repository that tells your AI tool how this project works: where secrets live, what it must never change, how to run the checks.
Why it matters: without rules, every new chat starts from zero and repeats the same mistakes. With them, the AI follows your project's decisions instead of guessing.
How to fix: write one page of rules, short and specific: "never put keys in frontend code", "every table needs RLS", "run the tests before you finish". Add a rule whenever the AI gets something wrong twice.
Look for: a CI pipeline (GitHub Actions, for example) that builds the app, runs the tests and scans for secrets on every push.
Why it matters: rules in a document are advice. A failing check is a wall. AI tools can quietly break things that worked yesterday, and only an automatic check catches that every time.
How to fix: add a pipeline with at least four steps: install, build, test, secret scan. Make a red check block merging.
Look for: whether your AI tool, or anyone, can push straight to the branch that deploys to production, and whether there is a staging copy to try changes on first.
Why it matters: one bad change pushed straight to production is a public outage or a data leak, with no second look.
How to fix: protect the production branch so changes arrive through pull requests with passing checks, and deploy every change to a staging environment before production.
All 25 done: you are in better shape than most launched apps. Keep the checks running as the app grows.
A few gaps: fix the secrets, access and database checks (1 to 12) first. They are the ones that leak data.
Many gaps, or you're not sure: that's normal for an app built fast with AI, and it's exactly what a Ship Safe audit covers. We go through all of this and more, rank what we find, and tell you what to fix first.