Blueprints/The Vibe Coding Security Checklist: 30 Checks Before You Launch
Playbook · Lovable, Bolt, Replit and Supabase

The Vibe Coding Security Checklist: 30 Checks Before You Launch

Thirty checks, each with the exact query, search or menu path to run, so your AI-built app does not ship with its database open.

An AI builder produces code that runs, and nobody has tried to break it yet. The two mistakes that show up again and again in apps shipped from Lovable, Bolt and Replit are a secret key sitting in browser code and a Supabase table with row-level security switched off, and either one lets a stranger with your project URL read every user's data. Supabase's own docs say the publishable key is safe to expose only when RLS is on for every table, and that the secret key bypasses every policy you have. This checklist is 30 checks in six groups: keys and secrets, database and RLS, auth, payments and webhooks, inputs and files, deployment and monitoring. Each check says what to look for, how to check it (a SQL query for the Supabase SQL editor, a search across your project, a dashboard path from the tool's current docs) and what a passing result looks like. Three ready-to-paste prompts make the builder's own agent audit its code and report before it fixes anything. A testing module shows how to prove with two accounts that one user cannot read another's rows, and how to check the browser's network tab for a leaked key. We built it from Supabase's RLS, API key and production security docs, the Lovable, Bolt and Replit docs on secrets and scanning, the OWASP Top 10 and the public checklists that cover vibe-coded apps, then ran the SQL and the prompts against a sample project with planted holes. It does not replace a professional security review. If your app stores health, financial or children's data, or moves money, pay for one before launch. With the checklist done first, that review can spend its time on problems specific to your app.

30checks in six groups
7SQL queries for the Supabase SQL editor
3security-review prompts to paste into your builder
0tools to buy
Read Supabase's RLS docs

Most of the critical checks come back to one page. Read it once, then run checks 06 to 11 in your SQL editor.

PlaybookLovable, Bolt, Replit and Supabase
1
2
3

Step-by-step playsto run

Step-by-step plays you can run this week.

What it does for you

Starts with the two holes that leak everything

A secret key in client code and a table with RLS off are the first checks, with the exact search and SQL query to find them in under ten minutes.

Every check says how to check it

No "make sure your app is secure" advice. Each item names a query, a grep pattern, a dashboard path from the tool's docs, or a request to make, and what a pass looks like.

Makes the builder audit its own work

Three prompts for Lovable, Bolt or Replit chat that ask the agent to report before it fixes, with file paths, severity and the SQL it used, so you can verify it.

Tests you can run in thirty minutes

A testing module with a two-account RLS test, a curl request against your REST endpoint, a network-tab check for leaked keys and a fake webhook post.

Tool-agnostic where it can be

Keys, RLS, webhooks, inputs and uploads are the same problem in every builder. Where a path differs, the Lovable, Bolt and Replit versions are given side by side.

What is included

Thirty checks in six groups, three review prompts written in full, what they caught in our test, a testing routine, a going-live sequence and two FAQs.

01

Checks 01 to 05: keys and secrets

The fastest way to lose every user's data is a key with service_role privileges in code that ships to the browser. Supabase's docs say the secret key bypasses every RLS policy. These five take about ten minutes.

  • 01. No secret key in client code. How: search the project for sb_secret_, sk_live_, sk_test_, whsec_ and any long string starting eyJ. Decode the middle segment of each eyJ string and look for "role":"service_role". Good: only sb_publishable_ (or the legacy anon key) reaches the browser.
  • 02. No secrets behind a public prefix. How: open .env and every config file and list variables starting with VITE_, NEXT_PUBLIC_ or REACT_APP_. Supabase's docs warn that bundlers ship these to the browser. Good: public-prefix variables hold only the project URL and the publishable key.
  • 03. Secrets live in the builder's secrets store, not in files. Lovable: More > Settings > Secrets (Edge Functions only). Bolt: database icon > Secrets (server functions only). Replit: Tools > Secrets, plus Production app secrets under Publishing. Good: payment, AI and SMTP keys exist only there.
  • 04. Any key ever pasted into chat, a file or a commit has been rotated. Supabase's guide: fix the leak first, create a new secret key in Settings > API Keys, replace it everywhere, then delete the old one. Stripe: roll the signing secret in Workbench > Webhooks. Good: the old value no longer works.
  • 05. One secret key per backend component. Supabase recommends a separate secret key for each server, function or job so a leak means rotating one key instead of all of them. Good: your Edge Functions, scheduled jobs and admin scripts each use a differently named key.
02

Checks 06 to 11: database and row-level security

Paste each query into the Supabase SQL editor. Supabase's RLS docs say a table in an exposed schema without RLS can be read and written by any role that holds a grant on it, and new tables in public start with full grants to anon and authenticated on existing projects.

  • 06. RLS is on for every table in public. SQL: select tablename from pg_tables where schemaname = 'public' and rowsecurity = false; Good: zero rows. Any table listed can be read and changed by anyone with your publishable key. Fix: alter table public.your_table enable row level security;
  • 07. Tables with RLS have policies for what the app does. SQL: select t.tablename from pg_tables t left join pg_policies p on p.schemaname = t.schemaname and p.tablename = t.tablename where t.schemaname = 'public' and t.rowsecurity and p.policyname is null; Good: only tables you meant to lock appear.
  • 08. No always-true policies on private data. SQL: select tablename, policyname, cmd, roles from pg_policies where schemaname = 'public' and (qual = 'true' or with_check = 'true'); Good: hits are only tables meant to be public, such as select on posts. Always-true update or delete for anon is a bug.
  • 09. Policies are scoped to the signed-in user and name a role. SQL: select tablename, policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'public'; Good: private tables use (select auth.uid()) = user_id with to authenticated; update policies carry both using and with check.
  • 10. Nothing trusts user_metadata. Users can edit it, so a role stored there proves nothing. SQL: select tablename, policyname from pg_policies where qual ilike '%user_metadata%' or with_check ilike '%user_metadata%'; Good: zero rows; roles live in app_metadata or in a table only the server writes.
  • 11. Security definer functions and views are reviewed. SQL: select p.proname from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.prosecdef; Good: each is needed, sets search_path = '' and checks auth.uid(). Views: with (security_invoker = true) on PG 15+.
03

Checks 12 to 16: auth and sessions

Builders often loosen auth settings to make testing easier and nobody turns them back. Paths below are from Supabase's current auth docs; the exact page is Authentication in the project dashboard.

  • 12. Email confirmation is on. Supabase: Authentication > Sign In / Providers > Email, Confirm email. Hosted projects default to on, but it is easy to switch off in testing. Good: a new account cannot sign in until the link is clicked, and any test phone numbers added while building are removed.
  • 13. Password rules and leaked password protection. Same Email provider page: set a minimum length (Supabase says under 8 characters is not recommended) and required character types. Leaked password protection checks HaveIBeenPwned and needs the Pro plan or above. Good: both on, or you know why not.
  • 14. Redirect URLs are an allowlist. Supabase: Authentication > URL Configuration. Site URL is your production domain and the redirect list holds only domains you own. Good: no wildcard entry that would let a crafted link send a magic link token to a site someone else controls.
  • 15. Admin and role checks happen in the database or on the server. How: search the code for isAdmin, role ===, localStorage and URL parameters that gate features. Good: every privileged action is enforced by an RLS policy or a server function that reads the role from a source the client cannot edit.
  • 16. Auth rate limits, CAPTCHA and SMTP are set for launch. Supabase: Authentication > Rate Limits shows the defaults. CAPTCHA is offered on signup, sign-in and password reset. The default email service is capped at 2 emails an hour. Good: limits reviewed, CAPTCHA on if you expect bots, own SMTP on.
04

Checks 17 to 20: payments, webhooks and server functions

A webhook handler that does not check the signature lets anyone mark themselves as paid with one curl command. Stripe's docs warn about exactly this. These apply to any payment or email provider that calls your app.

  • 17. Payment secret keys only run server side. How: grep for sk_live_ and sk_test_ anywhere outside server functions and the secrets store. Good: the browser only ever sees a publishable key or a checkout URL; the secret key is read with Deno.env.get or process.env inside a server function.
  • 18. Webhook signatures are checked. Stripe's docs: unverified, an attacker can post fake events that grant access or mark orders paid. Good: the handler checks the raw request text with constructEvent (constructEventAsync in Deno), the Stripe-Signature header and whsec_ secret, or 400.
  • 19. Duplicate webhook events are ignored. Stripe says the same event can arrive more than once and order is not guaranteed. Good: the handler writes each processed event ID to a table and skips IDs it has seen, so a replayed event cannot upgrade a plan or add credits twice.
  • 20. Server functions check who is calling. Supabase's docs note that its verify_jwt setting does not authenticate a caller sending only an API key. Good: each function touching user data reads the user's token, rejects a missing or bad one, and confirms ownership before a secret key changes a row.
05

Checks 21 to 25: inputs, APIs and files

Injection, stored cross-site scripting and open uploads are old problems that AI builders reintroduce because the generated code trusts whatever the form sends. Each check here is a grep plus a decision.

  • 21. Input is validated before it reaches the database. How: open each form handler and server function and look for a schema check (type, length, allowed values) before the insert or update. Good: numbers are numbers, strings have a maximum length, unknown fields are dropped so nobody sets a plan.
  • 22. No user text is joined into a query, shell command or AI prompt. How: grep for template strings and concatenation inside .rpc(, execute, exec( and prompt builders. Good: database calls use the client's query methods or parameters; untrusted text in an AI prompt is wrapped and labeled as data.
  • 23. User content is not rendered as raw HTML. How: grep for dangerouslySetInnerHTML and innerHTML. Good: zero hits on user-authored content, or the HTML is sanitized first. Stored cross-site scripting is how one user's message runs code in another user's browser.
  • 24. Uploads are limited and in the right bucket. Supabase says anyone with the URL can read a file in a public bucket. SQL: select id from storage.buckets where public; Good: sensitive documents sit in private buckets with policies on storage.objects; uploads have a type and size cap, renamed.
  • 25. CORS and exposed schemas are narrow. How: in each server function, check Access-Control-Allow-Origin is your domain rather than * on endpoints that use the caller's token. In Supabase, the Data API settings list exposed schemas. Good: only schemas the app needs are exposed, nothing internal.
06

Checks 26 to 30: deployment and monitoring

The builders now ship their own scanners and Supabase runs its Security Advisor on every project. Use them, then do the manual checks the scanners skip. Paths are from each tool's current docs.

  • 26. Run the builder's scanner and Supabase's Security Advisor. Lovable: Security view, Run quick scan and Run deep scan (quick also runs on publish). Bolt: database icon > Security (same checks as Supabase's advisor). Replit: Tools > Security Center. Supabase: Advisors > Security. Good: no Critical.
  • 27. Dependencies have no critical known vulnerabilities. How: run npm audit, or use the dependency scan built into the tools in check 26 (Replit's dependency scan is free; Lovable's quick scan includes a dependency audit). Good: nothing critical, or each remaining item has a written reason.
  • 28. Debug routes, test accounts and verbose errors are gone. How: signed out, visit /admin, /debug, /test and any route you used while building. Trigger an error on purpose. Good: no stack traces, file paths or table names in what the browser receives; the test logins you created are deleted.
  • 29. Supabase account and project are hardened. Supabase's production security checklist: MFA on your Supabase account (and GitHub if you sign in that way), a second organization owner, SSL enforcement and network restrictions in database settings, OTP expiry of 3600 seconds or less. Good: all done.
  • 30. Backups, logs and spend alerts exist before the first real user. Supabase says Free plan backups cannot be downloaded and point-in-time recovery is an add-on. Good: you know how you would restore, errors go to a log you read with no keys in it, and each AI and payment API has a spend alert.
07

Three security-review prompts for Lovable, Bolt or Replit chat

P1 covers secrets and keys, P2 the database and RLS, P3 auth, inputs, server functions and webhooks. Each spans three bullets (1/3 to 3/3): paste the three parts in order as one message, without the labels. All three ask the agent to report before it fixes, because an agent that fixes as it goes rewrites policies you did not ask it to touch.

  • How to use them: run P1, read the report, then run checks 01 to 05 yourself. Repeat for P2 and P3. Only then say "fix finding 1", one at a time, and re-run the matching check after each fix. In Lovable, also run the quick and deep scans in the Security view; its chat review is separate.
  • P1 (1/3): Act as a security reviewer for this project. Report first; change no code yet. Search every file, including .env, config, build output and server functions, for secrets: Supabase secret keys (sb_secret_), legacy service_role JWTs (eyJ strings whose decoded payload says role service_role),
  • P1 (2/3): Stripe keys (sk_live_, sk_test_, whsec_), AI provider keys, SMTP passwords and database connection strings. For each hit give the file path, the line, the first 6 characters of the value, and whether that file ships to the browser.
  • P1 (3/3): Then list every environment variable with a public prefix (VITE_, NEXT_PUBLIC_, REACT_APP_) and say whether its value is a secret. Finish with a numbered list of fixes in priority order, marking which ones need me to rotate a key at the provider.
  • P2 (1/3): Act as a database security reviewer for this project's Supabase schema. Report first; change nothing. Read every migration, SQL file and database call. Produce a table with one row per table in the public schema: name, RLS on or off, policies by operation (select, insert, update, delete),
  • P2 (2/3): the roles each policy names, and the exact condition. Flag: tables without RLS; tables with RLS and no policies; any policy whose condition is true or never references auth.uid(); policies that read user_metadata; security definer functions and views in public; public storage buckets.
  • P2 (3/3): For each flag, say what a stranger holding only the publishable key could do, and write the SQL fix. End with the queries you ran against pg_tables, pg_policies, pg_proc and storage.buckets so I can paste them into the SQL editor and check your answer.
  • P3 (1/3): Act as an application security reviewer. Report first; change nothing. Check: (1) every route that should need sign-in, and whether the database or a server function enforces it, not hidden UI; (2) any admin or role check that trusts the client: localStorage, URL parameter, user_metadata;
  • P3 (2/3): (3) every server function or API route: does it validate input types and sizes, does it act with the caller's token or a secret key, does it confirm the caller owns the record it changes; (4) every webhook handler: does it verify the signature on the raw body and skip duplicate event IDs;
  • P3 (3/3): (5) any place user text is rendered as HTML or joined into a query, shell command or AI prompt; (6) uploads: type and size limits, public bucket or not. Per finding give file, line, severity, attacker impact, the fix. End with a verdict: ship, fix first, or get a professional review.
08

Worked example: what the prompts and queries caught

We built a small sample project with the mistakes we see most (a React front end on Supabase with a Stripe webhook), planted nine problems, then ran the seven SQL queries in a local Postgres and applied the three prompts as the reviewer. Everything below is from that run; the keys were fakes.

  • The sample: profiles (RLS on, scoped policies), orders (RLS off, holds card last-4), messages (always-true policy for anon), a delete_any_order security definer function, a run_sql function running any string, a public uploads bucket, a hardcoded service_role JWT, a VITE_ Stripe key, an open webhook
  • Check 01 found the planted service_role key only after decoding. A plain search for the text service_role returned nothing, because in a legacy JWT the role is inside base64. The eyJ search plus decode is the step that matters; we rewrote check 01 after this.
  • P1 reported three hits: the JWT in src/lib/supabase.ts (ships to the browser, critical, rotate), VITE_STRIPE_SECRET_KEY (public prefix, bundled, rotate) and STRIPE_WEBHOOK_SECRET in a committed .env (not bundled, but in source control, move to Secrets). It also noted adminClient held the public key.
  • Query 06 returned orders. Query 07 returned nothing (every RLS table had a policy). Query 08 returned the messages policy, cmd ALL, roles anon and authenticated. Query 11 returned delete_any_order and run_sql, both without search_path. The bucket query returned uploads.
  • P2 rated run_sql as the worst finding: a security definer function in public that executes any SQL string means anyone with the publishable key has full read and write access to the database, RLS or not. It wrote the drop statement first and the RLS fix for orders second.
  • Two-account test, before the fix: user B selecting from orders saw both users' rows including Alice's card last-4. Anonymous select on messages returned Alice's private message. B updating Alice's profile returned zero rows, the pass. With RLS on orders and a select-own policy, B saw only Bob's row.
  • P3 found: an admin page gated by localStorage.getItem("isAdmin") calling delete_any_order (critical; anyone can call it anyway); a search joining user text into SQL (critical); a post shown with dangerouslySetInnerHTML (high, stored XSS); a webhook with no signature check or event log (critical).
  • Its verdict on the sample was "fix first", with a note that the payments flow should get a professional review before real money moves. First drafts of P3 missed the duplicate-event check and the ownership question for server functions; both were added and the sample re-run.
09

Test it: prove RLS blocks another user and nothing leaks in the browser

Reading policies is not the same as watching them block a request. These tests take about thirty minutes on a published preview and need two throwaway accounts.

  • 1. Create two accounts, A and B, through your real signup flow (confirm the emails). As A, create one of everything a user can create: a profile, a record, an upload, a message. Note A's record IDs from the Supabase table editor.
  • 2. Signed in as B in a private browser window, try to open A's records by changing the ID in the URL or in the request. Good: the page shows nothing or an error, and the network response body is empty or a 4xx. If A's data appears, the policy for that table is wrong; go back to checks 06 to 09.
  • 3. Hit the REST API directly. In B's session, open DevTools > Network, click a request to your project's rest/v1 endpoint and copy the Authorization: Bearer token. Run: curl "https://REF.supabase.co/rest/v1/orders?select=*" -H "apikey: PUBLISHABLE_KEY" -H "Authorization: Bearer TOKEN". Good: B only.
  • 4. Repeat step 3 without the Authorization header (anonymous). Good: an empty array for every private table. If rows come back, that table either has RLS off or an always-true select policy for anon.
  • 5. Try to write as B against A's rows. Send a PATCH to the same endpoint with ?user_id=eq.A_ID and a JSON body, plus the header Prefer: return=representation. Good: an empty array and no change in the table editor. Also try a DELETE with the same filter.
  • 6. Check what the browser received. DevTools > Network, filter by supabase.co, open any request and read the apikey header. Good: it starts with sb_publishable_ or is your legacy anon key. Then search the built JavaScript (Sources tab, or view-source) for sb_secret_, sk_live_ and whsec_.
  • 7. Fake a webhook. Post an invented checkout.session.completed JSON to your webhook URL with curl and no Stripe-Signature header. Good: a 400 and no change in the database. Then send a real test event with stripe trigger and confirm it is processed once; resend the same event; it should be skipped.
  • 8. Signed out, visit every route you used while building and any admin page. Good: redirected or an empty page, and the Network tab shows no data returned for those routes.
10

Going live: the order to do it in, and the first week after

The checks are not all equal. This is the sequence that gets the dangerous ones closed first, plus what to watch once strangers are using the app.

  • Day before: run the three prompts (module 07) and the seven queries (checks 06 to 11 and 24). Fix every Critical finding first. Rotate every key that was ever in a file, chat or commit (check 04). Swap any legacy anon key in client code for a publishable key; Supabase deprecates them by end of 2026.
  • Launch day: run the builder's scanner and Supabase's Security Advisor (check 26) once more after your last edit. Lovable's quick scan runs on publish; Replit's publish flow also scans and can block on critical findings if that setting is on. Run the two-account test (module 09) on the published URL.
  • First week: read the Supabase Auth logs and your server function logs daily. Look for repeated failed sign-ins from one address, rows you did not expect, and spikes in AI or email spend. Supabase emails organization owners a weekly security summary; open it.
  • Every time the builder touches the schema: re-run checks 06 to 08. On existing projects, new tables in public start with full grants to anon and authenticated, and an agent in a hurry can leave RLS off. Supabase's docs show an event trigger that turns RLS on for new tables, as a backstop.
  • When to pay for a professional review: you store health, financial or children's data; you move money or hold balances; you have more than a handful of paying customers; or a customer's procurement team asks. A checklist covers the common holes; a tester looks for the ones specific to your app.
  • Write down what you skipped and why. A one-page note listing each check, pass or skipped, and the reason, is what a reviewer, a partner or an acquirer will ask for first.
11

Is vibe coding secure?

Short answer: not by default. The default output of an AI builder is not safe to put in front of strangers, and the builders say so themselves.

  • Vibe-coded apps are not secure by default, and the tools agree. Lovable's docs say its scans "cannot guarantee complete security" and recommend a professional review for apps with sensitive data. Replit's docs call its build-time check lightweight, not a full codebase review. Both ship scanners.
  • The failure pattern is consistent: the model optimizes for code that runs, and a table without RLS, a webhook without a signature check and a secret key in the front end all run fine. None of them shows up as a bug until someone looks.
  • Three things change the picture. The builders now run scans on publish and flag tables with RLS off. Supabase runs its Security Advisor on every project and emails owners weekly. And the two most damaging mistakes, exposed secret keys and open tables, are findable in ten minutes (checks 01 and 06).
  • What stays risky: anything a scanner cannot reason about. Whether an admin check can be bypassed, whether a webhook can be replayed, whether a user can change the price in a request. Those are checks 15, 19 and 21 in this list, and they are manual.
  • If you are choosing a builder for an app that handles real customer data, the security tooling is now a reasonable comparison point. Our Lovable vs Bolt guide at nocodefounders.com/guides/lovable-vs-bolt covers the backend options each one gives you.
12

Do I need row-level security in Supabase?

Yes, on every table in an exposed schema, if your app talks to Supabase from the browser. This is Supabase's position in its own docs, and the reason is how the publishable key works.

  • Yes. Supabase's API key docs say the publishable key is safe to put in a web page only because "Row Level Security decides what this client can reach, so enable it on every table before you deploy." The key identifies your project; RLS is the thing that stops one user reading another's rows.
  • What happens without it: the Data API exposes your tables to anyone who has your publishable key, which is anyone who opens your site and reads the network tab. A table without RLS answers every select, insert, update and delete they send.
  • RLS on with no policies locks the table completely, which is the safe starting point. You then add one policy per operation (select, insert, update, delete), named to a role, usually (select auth.uid()) = user_id for authenticated. Supabase's RLS page has the four policies written out.
  • The exceptions are narrow: a table that is public by design (posts anyone may read) can carry an always-true select policy, and nothing else. If your app only reaches the database through server functions with a secret key, Supabase's docs note you can disable the Data API; the functions decide.
  • Bolt's docs say it turns on RLS for tables it creates, flags always-true policies, and tells you to confirm RLS on tables you add. Lovable's quick scan flags tables without per-record access control. Re-run check 06 after each schema change. Our Supabase review: nocodefounders.com/tools/supabase

Ready to use today

Run the 30 checks before your first stranger signs up

Start with checks 01 and 06: search your project for a secret key and run one query for tables with RLS off. Those two take ten minutes and close the holes that leak whole databases. Then paste the three prompts into your builder, work through the rest of the list, and run the two-account test on the published app. Keep the one-page record of what passed and what you skipped.

Read Supabase's RLS docs

Free. Works for Lovable, Bolt and Replit apps on Supabase, and most of it applies to any app with a database behind a browser.