Guide Security checklist

Vibe-Coded App Security Checklist: Lovable, Bolt, and Supabase (2026)

This checklist is for founders who built an app with Lovable, Bolt, Replit, v0, or Cursor on top of Supabase and want to know, before real users arrive, whether their data is safe. Each step says what to check, how to check it yourself, and what the fix looks like. Run every check against an app you own. Don't test someone else's app without permission, even with a single curl command.

What should you check before launching a Lovable or Supabase app?

Short answer

Check five things before launch: Row Level Security is on for every table in the public schema, each policy limits rows to their owner, no secret key appears in your frontend or repo, storage buckets holding private files are private, and Supabase's Site URL points to your production domain. Then test everything while logged out.

Sources: Supabase Database Advisors, Supabase API keys, Supabase Storage Buckets, Supabase Redirect URLs.

Prefer a short, reusable version of steps 1 to 7? See the free Supabase RLS checklist.

What did the security scans find?

Two security vendors published scans this year. Both sell scanning tools, and they measured different things (one scanned code repositories, the other probed live sites), so don't compare their numbers with each other. Each is shown on its own below.

Sentris: public code repositories

What it measured: 2,144 public GitHub repositories that use Supabase, meaning code, not live apps. Published Aug 5, 2026. Vendor research; Sentris may become a referral partner.

In Sentris's scan of public GitHub repositories, 57% of Lovable repos had a table in the public schema with Row Level Security switched off, compared with 21.7% of Supabase repos with no AI‑tool marker. Sentris says this one check accounts for almost all of the gap. Sentris found leaked credentials in 11.3% of Lovable repos and 11.2% of repos with no AI‑tool marker, "the same rate, within noise". It found a public storage bucket in 38.7% of Lovable repos, compared with 10.5% of repos with no AI‑tool marker.

Source: Sentris, “We scanned 2,144 AI-built Supabase repos. 40.6% had a critical exposure.”

Symbiotic Security: live sites

What it measured: 1,072 live vibe‑coded sites that use Supabase, probed from the outside. Published Jun 2, 2026. Vendor research.

In Symbiotic Security's scan of 1,072 vibe‑coded sites that use Supabase, 172 let anyone delete database records without authentication, 172 allowed unauthenticated PATCH (modification), 39 had tables fully readable with the public Supabase key, and 14 allowed unauthenticated inserts.

Source: Symbiotic Security report

There is also a CVE on record, CVE‑2025‑48757: An insufficient database Row‑Level Security policy in Lovable through 2025‑04‑15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites. It has a CVSS 3.1 score of 9.3, assigned by MITRE, and it's tagged disputed: Lovable says each customer is responsible for protecting their app's data. It covers Lovable through April 2025 and doesn't describe Lovable today. Either way, the practical point is the same: securing your data is your job, not the builder's.

The Sentris numbers point to where to start. In its data, the gap between Lovable repos and repos with no AI‑tool marker comes from tables with RLS switched off, not from leaked credentials.

How does Supabase security work?

Your frontend talks to Supabase directly, using a key that ships in your JavaScript. Supabase says publishable (anon) keys are designed to be public. What protects your data is Row Level Security (RLS) together with grants: grants control whether a role can reach a table, and RLS controls which rows it can access. A second key, the service_role key or the newer secret key, is different. Supabase says: "A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control."

Sources: Supabase: API keys, Supabase: Securing your API.

So "is my key exposed?" is usually the wrong question. The right questions are "which key?" and "what do my policies allow?"

What are the 12 checks?

1.Every table in public has RLS enabled

Check. In the Supabase SQL editor:

select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity;

Any table listed has RLS off. You can also open Advisors, then Security Advisor, in the dashboard. It flags RLS disabled on a public table as an error (lint 0013): Anyone with your project URL can read, edit, and delete all data in this table because Row‑Level Security is not enabled. (Supabase: Database Advisors)

Fix. Enable RLS and add policies in the same change:

alter table public.your_table enable row level security;

Then add the policies from step 2 in that same migration. Supabase: Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies. (Supabase: Row Level Security)

2.Policies are written per operation and scoped to the owner

Supabase's Security Advisor flags always‑true policy conditions such as using (true) as a permissive RLS policy (lint 0024, a warning) (Supabase: Database Advisors). Supabase also notes that when a request has no signed‑in user (no access token, or the session has expired), auth.uid() returns null (Supabase: Row Level Security).

Our guidance: a policy of using (true) lets everyone in, and a check of auth.uid() is not null on its own lets any signed‑in user reach every other user's rows. Compare auth.uid() to the row's owner column instead, as below.

Fix. One policy per operation, limited to the authenticated role, comparing the row's owner column to the caller:

create policy "orders_select_own" on public.orders
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "orders_insert_own" on public.orders
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "orders_update_own" on public.orders
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "orders_delete_own" on public.orders
  for delete to authenticated
  using ((select auth.uid()) = user_id);

The update policy uses both clauses. In Supabase's explanation, using decides which existing rows can be updated, and with check decides what the resulting row may look like, which stops a user from reassigning user_id to someone else. If with check is left out, using is applied to both (Supabase: Row Level Security; PostgreSQL: CREATE POLICY). Our guidance: write both, so the rule for the new row is explicit.

The policies wrap the call as (select auth.uid()). Supabase says this makes Postgres run an initPlan that caches the result per statement instead of calling the function on each row, which improves RLS performance. It only works when the result doesn't depend on row data (Supabase: RLS performance and best practices).

Check. The two‑account test: sign in as user A, copy the ID of a record that belongs to user B, and try to load, edit, and delete it through the app or a direct API call. Every attempt should fail.

3.Views don't quietly bypass RLS

Supabase's Security Advisor flags a security definer view as an error (lint 0010) (Supabase: Database Advisors). Views bypass RLS by default because they run with the view owner's permissions, and in Supabase views are usually created by the postgres user (Supabase: Row Level Security; PostgreSQL: CREATE VIEW).

Fix. On Postgres 15 and above, create views with security_invoker = true, so the view follows the caller's RLS policies:

create view public.public_profiles
with (security_invoker = true) as
select id, display_name, avatar_url from public.profiles;

On older Postgres versions, revoke access to the view from anon and authenticated, or put it in an unexposed schema (Supabase: Row Level Security).

4.The logged‑out test returns only public data

Check. Open your live app, open DevTools, then the Network tab, and find a request to https://<project>.supabase.co/rest/v1/.... Copy the apikey header value. Then, from a terminal:

curl "https://<project>.supabase.co/rest/v1/<table>?select=*&limit=5" \
  -H "apikey: <public-key>"

Run it for every table. Private tables should return [] or a permission error. If one returns rows, anyone on the internet can do exactly what you just did. Run this only against an app you own.

5.No secret key anywhere in the frontend or the repo history

Supabase: A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control. Publishable (anon) keys are designed to be public (Supabase: API keys).

Check.

  • Search the built bundle, not just the source: run npm run build, then grep -rE "sk_live_|sb_secret_|service_role" dist/.
  • In Vite apps, the Vite docs say: Variables prefixed with VITE_ will be exposed in client‑side source code after Vite bundling. And: VITE_* variables should not contain sensitive information such as API keys. (Vite: Env Variables and Modes)
  • Search git history: git log -p | grep -E "sk_live_|sb_secret_|service_role".

If a secret was ever committed, GitHub says to revoke or rotate it first. Removing it from git history means rewriting history, and the old commits may still be reachable elsewhere afterwards, for example in clones, forks, cached views and pull requests (GitHub: Removing sensitive data from a repository).

Fix, in order (our guidance):

  1. Move the code that needs the secret to a server, such as a Supabase Edge Function or an API route.
  2. Create a new key.
  3. Deploy the new key everywhere it's used.
  4. Revoke the old key.

Revoke last, or production breaks while you're still deploying.

6.Storage buckets match the sensitivity of what's in them

Supabase Storage buckets are private by default. For a public bucket, anyone who possesses the asset URL can readily access the file. Supabase's examples of public content are profile pictures, public media and blog content (Supabase: Storage Buckets).

Check. In the dashboard, open Storage and look at which buckets are marked Public. Check whether a public bucket holds anything that belongs to one user, such as IDs, invoices, uploads or exports.

Fix. Make private buckets private and add policies on storage.objects.

7.From Oct 30, 2026, Supabase will require grants for new public tables in existing projects. Never GRANT ALL to anon

From October 30, 2026, Supabase will apply this change to existing projects: new tables you create in the public schema will need an explicit grant before the Data API (the auto‑generated REST and GraphQL layer) can see them. Existing tables keep their current grants and stay reachable. New projects have had this behavior by default since the May 30, 2026 rollout. Until October 30, the Security Advisor flags affected tables and shows the SQL to fix them (Supabase changelog).

In Supabase's words: If the grant is missing, Postgres rejects the query before RLS comes into play. A table with no grant returns error 42501 "permission denied for table" even if your policies are perfect. The risk is the quick fix: an AI tool suggesting grant all on table ... to anon. Grants control whether a role can reach a table, and RLS controls which rows it can access (Supabase: Securing your API). Once the grant exists, RLS is the only thing left guarding the table. So a grant on a table with RLS off makes it fully public.

Fix (our guidance): grant only what each role needs, keep RLS on with policies, and never GRANT ALL to anon. Supabase's own fix treats the grant, enabling RLS, and the policies as one unit, so put them in one migration:

revoke all on table public.orders from anon, authenticated;
grant select, insert, update, delete on table public.orders to authenticated;
alter table public.orders enable row level security;

Then add the four policies from step 2 in the same migration. Grant anon access only for data signed‑out visitors truly need, and usually only select.

More detail: Supabase error 42501 "permission denied for table": what it means and the safe fix.

8.Auth URLs point at production

Supabase says to change the Site URL from http://localhost:3000 to your production URL. It is critical for email confirmations and password resets. In production, Supabase recommends setting the exact redirect URL rather than wildcards (Supabase: Redirect URLs).

Check and fix.

  • Supabase, then Authentication, then URL Configuration: set Site URL to your production domain.
  • Add your production domain to Redirect URLs. Use exact URLs for production.

9.Payment webhooks verify signatures, and payment state comes from the server

If your app marks a user as "paid" because the browser says so, anyone can mark themselves paid.

Check. Find where your app sets the paid or subscribed flag. It should only happen in a server‑side handler for a verified Stripe webhook, not in client code after redirecting back from Checkout.

Fix. Verify the Stripe-Signature header with your webhook signing secret before trusting the event (Stripe: Webhooks).

Stripe says webhook endpoints might occasionally receive the same event more than once. Log the IDs of events you've processed and skip any you've already logged, so a repeat doesn't grant access or send an email twice (Stripe: Webhooks).

10.Edge Functions check who's calling

OWASP Top 10:2025 keeps Broken Access Control at number 1 (A01). Its listed forms include an accessible API with missing access controls for POST, PUT, and DELETE (OWASP: A01:2025 Broken Access Control). Edge Functions and API routes are reachable by URL, so each one has to check who is calling.

Check. For each function, ask: what stops a stranger from calling this with arbitrary input? Acceptable answers are a verified user JWT (then check the user is allowed to act on the specific record), a verified webhook signature, or a shared secret for cron jobs. "Nobody knows the URL" is not one of them.

Supabase checks a caller's JWT for you by default (verify_jwt = true), and says to leave it on for functions your users call. Deploying with --no-verify-jwt turns that check off. For external webhooks such as Stripe, Supabase says to keep verify_jwt = false and verify the provider's signature inside the handler (Supabase: Functions auth; Supabase CLI: functions deploy).

11.Rate limits and abuse controls on anything that costs money or sends mail

Our guidance: endpoints that call a paid AI API, send email or SMS, or create accounts can be run up by a script. Turn on CAPTCHA or bot protection for sign‑up if you see abuse, cap per‑user usage of paid features on the server, and set spending limits with your AI and email providers.

12.You know what's deployed

The builder's preview, your GitHub repo, and production can drift apart. After any security fix, confirm it's live: re‑run the logged‑out test (step 4) and the two‑account test (step 2) against production, not preview.

What can I check in 15 minutes?

If you only have a few minutes today:

  1. Run the RLS query in step 1.
  2. Run the logged‑out curl in step 4 against your three most sensitive tables.
  3. Grep your built bundle for service_role, sb_secret_, and sk_live_ (step 5).
  4. Check which storage buckets are public (step 6).
  5. Add one line to your AI tool's project instructions:

Every new table must enable RLS, grant only needed operations, and have one owner‑scoped policy per operation, in the same migration.

If any of those turn something up, fix it before you announce the app.

Is this checklist a penetration test?

No. It's not a penetration test, a compliance audit, or a guarantee. It covers the issues described by the scans and Supabase docs cited on this page, for AI-built apps on Supabase. Apps that handle health, payment-card, or children's data need more than this.

What if I'd rather have it done?

PatchAndShip fixes these issues in Lovable, Bolt, Replit, and Cursor apps:

  • Security Fix, $149: RLS fixes, storage policies, a bundle key scan, key rotation guidance and a short report.
  • Fix & Deploy, $249: everything in Security Fix plus production deploy, env vars, auth redirect URLs, custom domain with SSL and a smoke test.

Both are fixed price. Security Fix: every critical issue on our checklist fixed within 48 hours of working access. Fix & Deploy: fixed and live on your host within 48 hours. Otherwise, a full refund if you ask within 14 days of purchase. AI agents draft the changes, and a human engineer approves every change.

Want to know where you stand first? PatchAndShip's free mini check is passive: it looks only at the public homepage, its scripts and its security headers (for exposed secret keys, missing headers and publicly listed storage links), and nothing else: no database queries, no logins, no changes. It returns a one‑page summary within 24 hours.

Where do these facts come from?

  1. Sentris: We scanned 2,144 AI-built Supabase repos. 5 Aug 2026. Vendor research.
  2. Symbiotic Security: We Scanned 1,072 Vibe Coded Apps. 2 Jun 2026. Vendor research.
  3. NVD: CVE-2025-48757. Score assigned by MITRE; disputed by Lovable.
  4. Supabase Docs: Database Advisors, Securing your API, API keys, Storage Buckets, Redirect URLs.
  5. Supabase changelog: Tables not exposed to Data and GraphQL API automatically. Published 28 Apr 2026.
  6. Vite: Env Variables and Modes, OWASP: A01:2025 Broken Access Control, Stripe: Webhooks.
  7. Supabase Docs: Row Level Security, RLS performance and best practices, Functions auth, CLI: functions deploy.
  8. PostgreSQL: CREATE POLICY, PostgreSQL: CREATE VIEW.
  9. GitHub Docs: Removing sensitive data from a repository.

Sources checked . Supabase is deprecating the anon and service_role keys by the end of 2026, in favor of sb_publishable_ and sb_secret_ keys (Supabase: API keys), and from October 30, 2026 it plans to change how new tables are exposed. Check the linked docs for the current state.