Founder & Lead Engineer, RAITHub
Before launching a vibe-coded app, run 20 pass/fail checks in six areas: data access, secrets, authentication, input handling, platform hardening and recovery. Every check below comes with a test you can run in minutes. Fix every failure in the first two areas before a single real user signs up, because those are the ones that expose other people's data.
"Vibe-coded" here means an app built mostly by prompting an AI tool such as Lovable, Bolt, Cursor or Replit, usually on Supabase or a similar backend. The checks are written for a founder or a developer with a browser, a terminal and an hour. They are pass/fail on purpose: a check you cannot fail is not a check.
Why does a vibe-coded app need its own security checklist?
Because the tools optimise for code that runs, and security failures do not stop code from running. Veracode's 2025 GenAI Code Security Report found that 45% of the AI-generated code samples it tested introduced an OWASP Top 10 vulnerability. The public example for app builders is CVE-2025-48757: Lovable projects whose missing row-level security let anyone read data with the public key. The researcher's statement counts 170 affected projects out of 1,645 analysed.
The list below maps onto the OWASP Top 10:2025, where Broken Access Control is A01 and Security Misconfiguration is A02, but it is ordered by what a generated app usually gets wrong first. Built-in scanners help: Lovable's security features include a scan before publishing and a deeper on-demand scan. Lovable also says these tools "do not replace a thorough security review", and several checks below, such as the second-user test, need a person or a written test. For the wider production picture, including tests, cost and config drift, see how to make a Lovable, Bolt or Cursor app production-ready. This page is the security gate only.
What are the 20 checks?
Checks 1 to 5 cover data access, 6 to 8 secrets, 9 to 12 authentication, 13 to 15 input and output, 16 to 18 platform hardening, and 19 and 20 recovery.
| No. | Check | How to test it | Pass when |
|---|---|---|---|
| 1 | Row-level security is on for every exposed table | Supabase Security Advisor, or query pg_class for public tables where relrowsecurity is false | Zero ERROR findings; the query returns no rows |
| 2 | No write policy is true | List pg_policies; look at insert, update and delete rows | Every write policy compares the row to the signed-in user or tenant |
| 3 | User B cannot reach user A's records | Copy a record ID as A; request it as B in a private window, through the app and the API | B gets nothing, or a 403 or 404 |
| 4 | File storage is private unless meant to be public | List buckets; open a private file's URL while signed out | Signed-out request fails; uploads are limited to the user's own folder |
| 5 | Views do not bypass row-level security | Security Advisor "security definer view" findings | None, or each view uses security_invoker = true |
| 6 | No secret in the front-end bundle | Build, then search the output for service_role, sb_secret_, sk_live_, sk- | No matches; only publishable keys in the browser |
| 7 | No secret in git history | git grep across git rev-list --all; check the repository's secret scanning alerts | No live secret anywhere in history |
| 8 | Every paid key has a spending cap | Open each provider's billing settings | Monthly caps and alerts set for AI, email, SMS and maps providers |
| 9 | Email confirmation is on and one-time codes expire | Sign up with a new address; try logging in before confirming | Unconfirmed accounts cannot sign in; OTP expiry is one hour or less |
| 10 | Roles are checked on the server | As a normal user, call an admin endpoint or edit your own role field directly | Refused; roles live where only the server can write them |
| 11 | Sign-up, sign-in and reset resist bots | Submit the sign-up form 20 times in a minute | CAPTCHA or a rate limit stops it |
| 12 | The accounts that run the app use MFA | Check Supabase, GitHub, hosting, domain registrar and the AI tool itself | Every admin account has multi-factor authentication |
| 13 | Every write is validated on the server | Call the API with an empty field, 10,000 characters and an extra "role": "admin" | Rejected with a 400; unknown fields ignored or refused |
| 14 | Errors do not leak internals | Break a request on purpose and read the response | A plain message; no stack trace, SQL or file path |
| 15 | User content cannot run as code | Save <img src=x onerror=alert(1)> as a name or comment and view it | It shows as text; no alert box |
| 16 | Security headers are sent | curl -sI your live URL | HSTS, CSP, frame protection, nosniff, Referrer-Policy and Permissions-Policy present |
| 17 | Login, sign-up, reset and paid endpoints are rate limited | Hit each 20 to 50 times in a minute | HTTP 429, or the provider's limit, before the damage is done |
| 18 | Dependencies have no known high or critical flaws | npm audit --omit=dev in a clean clone | No high or critical findings, or each one explained in writing |
| 19 | The database can be rebuilt and restored | Rebuild an empty database from migration files; restore a backup to a scratch project | Both succeed, and you know how long a restore takes |
| 20 | Security-relevant errors reach a person | Trigger a failed login burst and a server error | An alert arrives somewhere a person reads daily |
How do I run the data access checks?
Checks 1 to 5 decide whether one user can see another's data, so they come first. On Supabase, the dashboard's Security Advisor covers most of checks 1, 2 and 5, and its advisors documentation says to rerun it after every fix. The query for check 1:
select c.relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
Check 3 is the one no scanner does for you. It is the insecure direct object reference test, or IDOR: change an ID in a request and see whose data comes back. Do it through the app's own API routes as well as the database API, because a server route that uses the secret key skips row-level security entirely.
For check 4, Supabase's storage access control guide notes that public buckets "are already publicly accessible", and that uploads need a policy. A policy that confines each user to a folder named after their ID:
create policy "Users upload to their own folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'uploads' and
(storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
How do I check for leaked secrets before launch?
Search the build output, because that is exactly what a visitor downloads. For a Vite-based app, which older Lovable projects are, the output folder is dist. This CI step fails the build if a secret prefix appears in it:
npm run build
if grep -rEq "sb_secret_|service_role|sk_live_|rk_live_|sk-proj-|sk-ant-" dist; then
echo "Secret found in build output"
exit 1
fi
Vite's environment variable guide explains why this matters: variables prefixed VITE_ "will be exposed in client-side source code after Vite bundling". The same applies to NEXT_PUBLIC_ in Next.js. For check 7, GitHub's secret scanning scans a repository's entire history on all branches, and runs automatically on public repositories.
Which authentication settings matter most?
Email confirmation, short-lived codes, bot protection and server-side roles. Supabase's production checklist recommends enabling email confirmations, setting OTP expiry to "3600 seconds (1 hour) or lower", offers CAPTCHA on sign-up, sign-in and password reset, and asks you to protect the Supabase account itself with MFA. It also lists a default limit of 2 emails an hour on its email endpoints and recommends a custom SMTP server, which you will need before launch anyway.
Check 10 catches a common generated pattern: a user's role stored somewhere the user can edit, and checked only by hiding a button. Store roles in a table that only server code can write, and check them on the server or in a policy for every privileged action.
Which security headers should the app send?
Six, all recommended by OWASP's HTTP headers cheat sheet. This website sends all six on every response, set in its Next.js config:
| Header | This site's value | What it stops |
|---|---|---|
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Downgrade to plain HTTP, for two years |
| Content-Security-Policy | default-src 'self', frame-src 'none', object-src 'none', base-uri 'self', form-action 'self', plus named analytics hosts | Scripts, frames and form posts from places you did not approve |
| X-Frame-Options | DENY | Clickjacking through framing |
| X-Content-Type-Options | nosniff | Browsers guessing file types |
| Referrer-Policy | strict-origin-when-cross-origin | Full URLs leaking to other sites |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | Device features the site never uses |
An honest note: this site's content security policy still allows 'unsafe-inline' scripts, which OWASP advises against. It is a known gap, not a recommendation to copy.
A generated single-page app hosted on Vercel can send the same headers from a vercel.json file. Replace the Supabase host with your own project's, and add preload to HSTS only once you are sure every subdomain serves HTTPS, because preloading is slow to undo:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'; connect-src 'self' https://YOUR_REF.supabase.co wss://YOUR_REF.supabase.co; img-src 'self' data: https://YOUR_REF.supabase.co; frame-ancestors 'none'; object-src 'none'; base-uri 'self'" }
]
}
]
}
Then run check 16 against the live URL:
curl -sI https://your-app.example | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"
Six lines back is a pass. Expect to adjust the CSP once or twice: open the browser console, and each blocked resource names the directive to widen.
What should be rate limited, and how hard?
Anything an attacker can repeat for profit: login, sign-up, password reset, contact forms, uploads and every endpoint that calls a paid API. The limits on this website are a worked example, running on serverless hosting with a Postgres-backed limiter and no Redis:
- Login, three buckets: 5 attempts a minute per email and IP, 20 a minute per IP, and 50 per 15 minutes per email across all IPs. One bucket keyed only on email would let anyone lock the real user out.
- Admin API: 30 writes a minute per IP.
- Media uploads: 10 a minute per IP, because each upload costs money at the image host.
- Public API: 120 requests a minute per IP.
- Confirmation emails: 3 per recipient address per day, so the contact form cannot be used to flood someone else's inbox.
The code, the trade-offs and why an in-memory counter fails on serverless are in the rate limiting explainer. For a vibe-coded app, the paid endpoints matter most: an AI feature that anyone can call in a loop is a bill, not just a vulnerability.
What about backups, migrations and alerts?
Checks 19 and 20 do not stop an attack. They decide how bad the day after is. If the schema only exists in a dashboard, nobody can rebuild it; if the only backup has never been restored, it is a hope. Supabase's production checklist recommends Point in Time Recovery once a database is expected to pass 4 GB. And an alert that goes to an unread inbox is not an alert: route failed-login bursts and server errors somewhere a person looks every day.
Which failures block launch, and which can wait a week?
| Failed check | Launch? | Why |
|---|---|---|
| 1 to 7, 10, 13 | No | Each one lets a stranger read or change data, or spend your money |
| 8, 9, 11, 12, 15, 16, 17 | Only with a dated fix plan | They raise the cost of an attack; fix within days, before paid traffic |
| 14, 18, 19, 20 | Yes, fix in the first sprint | They limit damage and recovery time rather than prevent access |
For everything outside security that a launch needs, such as critical-path tests, performance and monitoring, use the pre-launch QA checklist alongside this one.
Why RAITHub for this
- The checks become tests. A one-off audit ages the day the next prompt changes the code. RAITHub turns failing checks into automated tests gated in CI, as on Sundor Skin, where the build fails if a buyer-scoped table lacks a row-level-security policy and an IDOR suite tries to read other buyers' data.
- Worked examples you can inspect. The headers and rate limits above are running on this site today, with their gaps stated.
- Fixed scope. A free 15-minute technical audit, then a written, fixed quote, with the fixes delivered as pull requests you own.
RAITHub has not published a case study of hardening an AI-generated app, so this is engineering guidance, not a claim of past rescues.
When to use a tool instead
- Every check passes and someone owns the app. Keep the Security Advisor, dependency audit and the bundle search in CI, and rerun this list before each major release.
- Only one or two checks fail. Each has a documented fix linked above; a single issue is a job for the fix one issue page, not a rescue.
- You need an independent penetration test or a certified vendor. RAITHub is not SOC 2 or ISO 27001 certified and does not sell penetration testing.
If several checks in the first two groups failed, the problem is usually structural. Code Rescue is built for that.
Last reviewed: 29 September 2026. Veracode, OWASP, Supabase, Vite, GitHub and CVE sources checked on 29 September 2026.
Send the checklist results, pass or fail per line, through Code Rescue on the contact form, and the free 15-minute audit starts from your findings.
Frequently asked questions
What is the most common security issue in vibe-coded apps?
Broken access control: one user can read or change another user's data, usually because row-level security is missing or a policy allows everything. It is A01 in the OWASP Top 10:2025, and it was the root cause of CVE-2025-48757.
How long does this security checklist take?
About one to two hours to run all 20 checks on a small app, if you have access to the database, the repository and the hosting settings. Fixing failures takes longer, from minutes for a header to days for access control across many tables.
Are AI tool security scanners enough before launch?
They are a useful first pass, not a gate. Lovable's own documentation says its scans do not replace a thorough security review. Checks such as the second-user test and server-side role checks need a person or an automated test.
Which security headers does a Lovable or Bolt app need?
Strict-Transport-Security, Content-Security-Policy, X-Frame-Options or a frame-ancestors directive, X-Content-Type-Options, Referrer-Policy and Permissions-Policy. Static hosts such as Vercel let you set them in a config file.
Do I need rate limiting if Supabase already limits auth?
Yes, for everything Supabase does not cover. Its auth endpoints have built-in limits, but your own Edge Functions, API routes and any endpoint that calls a paid API need limits you set yourself.
Can I launch with some checks failing?
Not with failures in data access, secrets, server-side roles or input validation. Those expose data or money. Header, bot-protection and recovery gaps can ship with a dated fix plan, fixed before paid traffic arrives.
Related posts
"RLS Disabled in Public" in Supabase: What It Means and How to Fix It
13 min readLovable App Exposing API Keys or service_role? Fix It in 30 Minutes
13 min readSaaS Security Best Practices: How to Pass a Customer's Security Review
12 min readReady to discuss your project?
Book a free 15-minute technical audit with our engineering team.