PreflightVibe logo PreflightVibe Free scan

Sample report with demo data. The app, URL, Supabase project and keys are placeholders. Every key is fake and masked the same way your real report masks them. Generated by running our real scanner against a mock app, so every finding type here is one we really produce.

Full launch report

https://your-app.lovable.app/

Scanned Oct 18, 2026 · 17 findings · passive read-only

F

Grade

3
critical
1
high
4
medium
6
low
3
info

Biggest risks

Leaked keys and data anyone can read. Fix these before you launch.

The full report has the evidence and copy-paste fixes (exact RLS SQL / Firebase rules + prompts for Cursor, Claude Code, Lovable, Bolt and v0).

What we checked
  • Scanned your HTML and 1 same-origin JS file for leaked keys (Stripe, OpenAI, Anthropic, AWS, GitHub, Slack, Google, Supabase secret keys).
  • Supabase: tested 6 tables (of 6 listed) and 1 storage bucket with your public anon key.
  • Libraries: detected jQuery 3.4.1 and compared them with our list of known vulnerable versions.
  • CORS: sent one request with a foreign Origin header to see if your server echoes it back with credentials allowed.
  • Email DNS: skipped (platform subdomain such as *.vercel.app, where you cannot set SPF/DMARC). Also checked for /.well-known/security.txt.

After fixing: the free re-check

The owner turned on RLS for profiles and payments, rotated the Stripe key and locked the invoices bucket, then pressed "Re-check my site". This is what the re-check shows:

Before / after

Grade F → D (score 0 → 47; first scan Oct 18, 2026)

Fixed 4

  • CRITICAL Stripe secret key exposed in JS:index-Dk3x9QpA.js
  • CRITICAL Supabase table "profiles" readable without login (columns include email, phone, stripe_customer_id)
  • CRITICAL Supabase table "payments" readable without login (columns include stripe_customer_id)
  • HIGH Supabase Storage bucket "invoices" lets anyone list its files

Still open 13

  • MEDIUM Missing Content-Security-Policy
  • MEDIUM jQuery 3.4.1 has known vulnerabilities (CVE-2020-11022)
  • MEDIUM Source map publicly downloadable
  • MEDIUM Supabase table "posts" is publicly readable (fine only if meant to be public)
  • LOW Missing X-Frame-Options / frame-ancestors
  • LOW Missing X-Content-Type-Options
  • LOW Missing meta description
  • LOW CORS reflects any origin (without credentials)
  • LOW Supabase schema lists 6 tables to anonymous users
  • LOW Supabase Storage bucket list visible to anonymous users (1)
  • INFO Supabase project URL in client
  • INFO Supabase anon key in client (expected)
  • +1 more

New 0

  • None
Fix prompts for:
CRITICAL

Stripe secret key exposed in JS:index-Dk3x9QpA.js

Pattern matching Stripe secret key found in JS:index-Dk3x9QpA.js. Redacted: sk_liv…5625.

Evidence: sk_liv…5625

Plain-English fix

A Stripe secret key (sk_ / rk_) is in your public JavaScript, so anyone can copy it and use it on your bill or your data. Rotate it in Stripe Dashboard > Developers > API keys (roll the key), move every call that needs it to server-side code, and keep the key in a private environment variable. Only the publishable key (pk_) belongs in the browser.

Prompt for Cursor

Security fix: a Stripe secret key (sk_ / rk_) is exposed in the frontend bundle.
1) Find every place the key is read or hard-coded (search for the key prefix, STRIPE_SECRET_KEY, and any VITE_/NEXT_PUBLIC_/REACT_APP_/EXPO_PUBLIC_ variant of it).
2) Move every call that uses it into server-only code (API route, server action, or Supabase Edge Function) that reads STRIPE_SECRET_KEY from a private env var. Never prefix it with VITE_ or NEXT_PUBLIC_.
3) Change the frontend to call that server endpoint instead, and require a logged-in user plus basic rate limiting on it.
4) Remove the old public env var from .env files and from the hosting dashboard.
IMPORTANT: removing the key from code is not enough. The old key is already public, so I will rotate it myself in Stripe Dashboard > Developers > API keys (roll the key); remind me at the end.

How to check it worked: Rebuild, open the live site, and search the JS files in DevTools > Sources for the key prefix. It should not appear.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
CRITICAL

Supabase table "profiles" readable without login (columns include email, phone, stripe_customer_id)

A read-only GET to https://abcdefghijklmnopqrst.supabase.co/rest/v1/profiles?limit=1 with only the public anon key (which ships in your JavaScript) returned a row, so anyone can download this table. Row Level Security is off or has a policy that allows anonymous reads. Sensitive-looking columns: email, phone, stripe_customer_id. (We did not store or display any row values.)

Plain-English fix

Anyone can download rows from "profiles" using the public anon key that ships in your JavaScript. Turn on Row Level Security for it and add policies so that each signed-in user can only read and edit their own rows; logged-out visitors get nothing.

Supabase SQL editor (paste and run)

-- Fix anonymous access to public.profiles
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.profiles enable row level security;
-- 2) First delete any "allow all" / "true" policies on profiles: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'profiles';)
-- 3) Logged-out visitors get nothing
revoke all on table public.profiles from anon;
-- 4) Each signed-in user can read and edit only their own row(s)
create policy "profiles_select_own" on public.profiles for select to authenticated using ((select auth.uid()) = id);
create policy "profiles_insert_own" on public.profiles for insert to authenticated with check ((select auth.uid()) = id);
create policy "profiles_update_own" on public.profiles for update to authenticated using ((select auth.uid()) = id) with check ((select auth.uid()) = id);

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

-- Then check from a terminal:
-- curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/profiles?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Prompt for Cursor

CRITICAL: the Supabase table public.profiles is readable with only the anon key. It has sensitive columns: email, phone, stripe_customer_id.
Goal: each signed-in user can only read and edit their own rows; logged-out visitors get nothing.
1) Create a migration (supabase/migrations/<timestamp>_rls_profiles.sql) with exactly this SQL and apply it (supabase db push, or run it in the SQL editor):

-- Fix anonymous access to public.profiles
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.profiles enable row level security;
-- 2) First delete any "allow all" / "true" policies on profiles: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'profiles';)
-- 3) Logged-out visitors get nothing
revoke all on table public.profiles from anon;
-- 4) Each signed-in user can read and edit only their own row(s)
create policy "profiles_select_own" on public.profiles for select to authenticated using ((select auth.uid()) = id);
create policy "profiles_insert_own" on public.profiles for insert to authenticated with check ((select auth.uid()) = id);
create policy "profiles_update_own" on public.profiles for update to authenticated using ((select auth.uid()) = id) with check ((select auth.uid()) = id);

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

2) Find every supabase.from('profiles') call and make sure it runs with a logged-in session; move anything that must read other users' rows into a server route / Edge Function using the service role key.
3) Check every other table the app uses for the same problem.

How to check it worked: curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/profiles?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
CRITICAL

Supabase table "payments" readable without login (columns include stripe_customer_id)

A read-only GET to https://abcdefghijklmnopqrst.supabase.co/rest/v1/payments?limit=1 with only the public anon key (which ships in your JavaScript) returned a row, so anyone can download this table. Row Level Security is off or has a policy that allows anonymous reads. Sensitive-looking columns: stripe_customer_id. (We did not store or display any row values.)

Plain-English fix

Anyone can download rows from "payments" using the public anon key that ships in your JavaScript. Turn on Row Level Security for it and add policies so that logged-out visitors get nothing, signed-in users can only read their own rows, and rows are only written by the server/webhook with the service role key.

Supabase SQL editor (paste and run)

-- Fix anonymous access to public.payments
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.payments enable row level security;
-- 2) First delete any "allow all" / "true" policies on payments: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'payments';)
-- 3) Logged-out visitors get nothing
revoke all on table public.payments from anon;
-- 4) Signed-in users can read only their own rows
create policy "payments_select_own" on public.payments for select to authenticated using ((select auth.uid()) = user_id);
-- 5) No insert/update/delete policies on purpose: write these rows only from your server
--    or Stripe webhook using the service role key (never from the browser).

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

-- Then check from a terminal:
-- curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/payments?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Prompt for Cursor

CRITICAL: the Supabase table public.payments is readable with only the anon key. It has sensitive columns: stripe_customer_id.
Goal: logged-out visitors get nothing, signed-in users can only read their own rows, and rows are only written by the server/webhook with the service role key.
1) Create a migration (supabase/migrations/<timestamp>_rls_payments.sql) with exactly this SQL and apply it (supabase db push, or run it in the SQL editor):

-- Fix anonymous access to public.payments
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.payments enable row level security;
-- 2) First delete any "allow all" / "true" policies on payments: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'payments';)
-- 3) Logged-out visitors get nothing
revoke all on table public.payments from anon;
-- 4) Signed-in users can read only their own rows
create policy "payments_select_own" on public.payments for select to authenticated using ((select auth.uid()) = user_id);
-- 5) No insert/update/delete policies on purpose: write these rows only from your server
--    or Stripe webhook using the service role key (never from the browser).

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

2) Find every supabase.from('payments') call and make sure it runs with a logged-in session; move anything that must read other users' rows into a server route / Edge Function using the service role key.
3) Check every other table the app uses for the same problem.

How to check it worked: curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/payments?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
HIGH

Supabase Storage bucket "invoices" lets anyone list its files

A read-only list request on bucket "invoices" with only the anon key returned file entries. A storage.objects SELECT policy allows anonymous listing. (We did not store any file names.)

Plain-English fix

Anyone can see the list of files in your Supabase Storage bucket "invoices" with the public anon key. Remove the storage policy that lets anonymous users SELECT, and store private files under a folder named after the user's id with owner-only policies.

Supabase SQL editor (paste and run)

-- 1) Find the policy that lets anyone list files in "invoices"
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'objects';
-- 2) Drop the over-broad one(s), e.g.:
-- drop policy "<policy name>" on storage.objects;
-- 3) Owner-only access: files live under a folder named after the user id (<uid>/file.png)
create policy "invoices_read_own" on storage.objects for select to authenticated
  using (bucket_id = 'invoices' and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "invoices_upload_own" on storage.objects for insert to authenticated
  with check (bucket_id = 'invoices' and (storage.foldername(name))[1] = (select auth.uid())::text);
-- If these files are meant to be public (e.g. avatars), make the bucket public for downloads instead of adding an anon SELECT policy: public URLs work without one and nobody can list the folder.

-- Then check from a terminal:
-- curl -X POST "https://YOUR_PROJECT.supabase.co/storage/v1/object/list/invoices" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY" -H "Content-Type: application/json" -d '{"prefix":"","limit":1}'   # expect []

Prompt for Cursor

Supabase Storage bucket "invoices" can be listed anonymously. Write and apply a migration with this SQL (adjust the folder convention if files are not stored under <user id>/...):

-- 1) Find the policy that lets anyone list files in "invoices"
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'objects';
-- 2) Drop the over-broad one(s), e.g.:
-- drop policy "<policy name>" on storage.objects;
-- 3) Owner-only access: files live under a folder named after the user id (<uid>/file.png)
create policy "invoices_read_own" on storage.objects for select to authenticated
  using (bucket_id = 'invoices' and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "invoices_upload_own" on storage.objects for insert to authenticated
  with check (bucket_id = 'invoices' and (storage.foldername(name))[1] = (select auth.uid())::text);
-- If these files are meant to be public (e.g. avatars), make the bucket public for downloads instead of adding an anon SELECT policy: public URLs work without one and nobody can list the folder.

Then update upload code to save files at `${user.id}/<filename>`, and use createSignedUrl() for private downloads.

How to check it worked: curl -X POST "https://YOUR_PROJECT.supabase.co/storage/v1/object/list/invoices" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY" -H "Content-Type: application/json" -d '{"prefix":"","limit":1}'   # expect []

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
MEDIUM

Missing Content-Security-Policy

No CSP header. A baseline CSP reduces XSS impact.

Plain-English fix

Content-Security-Policy is missing. A CSP limits which scripts can run, which reduces the damage of an XSS bug. Start in report-only mode, then enforce.

Prompt for Cursor

Add a Content-Security-Policy header. Start with Content-Security-Policy-Report-Only so nothing breaks:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co; frame-ancestors 'none'; base-uri 'self'; object-src 'none'
List every third-party domain the app actually loads (analytics, fonts, Stripe, Supabase) and add them to the right directive. Check the browser console for violations, then switch to the enforcing Content-Security-Policy header (vercel.json headers, netlify.toml, or next.config.js headers()).

How to check it worked: curl -sI https://your-site.com | grep -i content-security-policy

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
MEDIUM

jQuery 3.4.1 has known vulnerabilities (CVE-2020-11022)

Your site ships jQuery 3.4.1 (seen in index-Dk3x9QpA.js). Known advisories for this version: CVE-2020-11022 / CVE-2020-11023: XSS when untrusted HTML is passed to .html(), .append() and similar methods (fixed in 3.5.0). Version detected from the shipped files; it can be wrong if the banner was edited.

Evidence: jQuery 3.4.1

Plain-English fix

Your site ships jQuery 3.4.1, which has known security advisories (CVE-2020-11022). Upgrade it (fixed in 3.5.0) and redeploy.

Prompt for Cursor

Upgrade a vulnerable client library: jQuery 3.4.1 is in the deployed bundle.
Advisories:
- CVE-2020-11022 (fixed in 3.5.0): https://nvd.nist.gov/vuln/detail/CVE-2020-11022
Find where jQuery comes from: a package.json dependency ("npm ls jquery" shows which package pulls it in), a CDN <script> tag, or a WordPress/theme bundle. Upgrade to 3.7.1 or later (npm install jquery@latest, or update the CDN URL), run "npm audit" for other advisories, rebuild and redeploy. Read the jQuery 3.5 upgrade guide: htmlPrefilter changed, so test pages that inject HTML.

How to check it worked: View the live JS in DevTools > Sources and confirm the jQuery version banner shows the new version, then re-scan.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
MEDIUM

Source map publicly downloadable

https://your-app.lovable.app/assets/index-Dk3x9QpA.js.map is reachable and looks like a source map. Disable production source maps.

Evidence: https://your-app.lovable.app/assets/index-Dk3x9QpA.js.map

Plain-English fix

JavaScript source maps are downloadable, which hands out your original source code (comments, file structure, sometimes secrets). Turn them off for production.

Prompt for Cursor

Disable public production source maps: Vite build.sourcemap = false (or "hidden" and upload to your error tracker); Next.js productionBrowserSourceMaps: false; CRA GENERATE_SOURCEMAP=false. Make sure no *.map files are in the deploy output.

How to check it worked: Open one of the .map URLs from the report: it should be 404.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
MEDIUM

Supabase table "posts" is publicly readable (fine only if meant to be public)

A read-only GET to https://abcdefghijklmnopqrst.supabase.co/rest/v1/posts?limit=1 with only the public anon key (which ships in your JavaScript) returned a row, so anyone can download this table. Row Level Security is off or has a policy that allows anonymous reads. (We did not store or display any row values.)

Plain-English fix

Anyone can download rows from "posts" using the public anon key that ships in your JavaScript. Turn on Row Level Security for it and add policies so that everyone can read it (it is public content), but only the author can create, edit or delete their own rows.

Supabase SQL editor (paste and run)

-- Fix anonymous access to public.posts
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.posts enable row level security;
-- 2) First delete any "allow all" / "true" policies on posts: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'posts';)
-- 3) Everyone may read (add "using (published = true)" instead of "true" if you have drafts)
create policy "posts_public_read" on public.posts for select to anon, authenticated using (true);
-- 4) Only the author can write
create policy "posts_insert_own" on public.posts for insert to authenticated with check ((select auth.uid()) = author_id);
create policy "posts_update_own" on public.posts for update to authenticated using ((select auth.uid()) = author_id) with check ((select auth.uid()) = author_id);
create policy "posts_delete_own" on public.posts for delete to authenticated using ((select auth.uid()) = author_id);

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

-- Then check from a terminal:
-- curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/posts?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Prompt for Cursor

CRITICAL: the Supabase table public.posts is readable with only the anon key.
Goal: everyone can read it (it is public content), but only the author can create, edit or delete their own rows.
1) Create a migration (supabase/migrations/<timestamp>_rls_posts.sql) with exactly this SQL and apply it (supabase db push, or run it in the SQL editor):

-- Fix anonymous access to public.posts
-- 1) Turn on Row Level Security (denies everything until a policy allows it)
alter table public.posts enable row level security;
-- 2) First delete any "allow all" / "true" policies on posts: Dashboard > Authentication > Policies
--    (list them with: select policyname, roles, cmd, qual from pg_policies where tablename = 'posts';)
-- 3) Everyone may read (add "using (published = true)" instead of "true" if you have drafts)
create policy "posts_public_read" on public.posts for select to anon, authenticated using (true);
-- 4) Only the author can write
create policy "posts_insert_own" on public.posts for insert to authenticated with check ((select auth.uid()) = author_id);
create policy "posts_update_own" on public.posts for update to authenticated using ((select auth.uid()) = author_id) with check ((select auth.uid()) = author_id);
create policy "posts_delete_own" on public.posts for delete to authenticated using ((select auth.uid()) = author_id);

-- Check: every table in public should show rowsecurity = true
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

2) Find every supabase.from('posts') call and make sure it runs with a logged-in session; move anything that must read other users' rows into a server route / Edge Function using the service role key.
3) Check every other table the app uses for the same problem.

How to check it worked: curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/posts?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

Missing X-Frame-Options / frame-ancestors

Page may be embeddable in clickjacking iframes.

Plain-English fix

X-Frame-Options / frame-ancestors is missing, so other sites can load yours in a hidden iframe (clickjacking). Set X-Frame-Options: DENY or CSP frame-ancestors 'none'.

Prompt for Cursor

Add the response header "X-Frame-Options: DENY" to every HTML response.
- Vercel: add it under "headers" for source "/(.*)" in vercel.json
- Netlify: add it to _headers or netlify.toml
- Next.js: add it in next.config.js async headers()
- Express: res.setHeader in a global middleware
Do not change anything else.

How to check it worked: curl -sI https://your-site.com | grep -i "X-Frame-Options"

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

Missing X-Content-Type-Options

Add nosniff to prevent MIME sniffing.

Plain-English fix

X-Content-Type-Options: nosniff is missing. Add it so browsers do not guess file types (MIME sniffing).

Prompt for Cursor

Add the response header "X-Content-Type-Options: nosniff" to every HTML response.
- Vercel: add it under "headers" for source "/(.*)" in vercel.json
- Netlify: add it to _headers or netlify.toml
- Next.js: add it in next.config.js async headers()
- Express: res.setHeader in a global middleware
Do not change anything else.

How to check it worked: curl -sI https://your-site.com | grep -i "X-Content-Type-Options"

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

Missing meta description

No meta description found. Add a one-sentence summary for search snippets.

Plain-English fix

No meta description. Add a one-sentence summary for search snippets and social previews.

Prompt for Cursor

Add <meta name="description" content="..."> (140-160 chars) to index.html, or metadata.description in the Next.js root layout.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

CORS reflects any origin (without credentials)

A GET to https://your-app.lovable.app/ with "Origin: https://evil.example" came back with Access-Control-Allow-Origin: https://evil.example. Cookies are not allowed, so this is low risk for public pages, but it usually means a catch-all CORS setting that could become dangerous if credentials are switched on or private endpoints share it.

Evidence: Access-Control-Allow-Origin: https://evil.example

Plain-English fix

Your server copies any Origin back into Access-Control-Allow-Origin. Without credentials this is low risk for public pages, but replace the catch-all with an allowlist so it never becomes a problem on private endpoints.

Prompt for Cursor

Tighten CORS: the server reflects any Origin header into Access-Control-Allow-Origin. Replace the reflect-all setting with an allowlist of your own domains (or omit CORS headers entirely on HTML pages that do not need them), add "Vary: Origin", and make sure no route combines a reflected origin with Access-Control-Allow-Credentials: true.

How to check it worked: curl -s -o /dev/null -D - -H "Origin: https://evil.example" https://your-site.com | grep -i access-control

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

Supabase schema lists 6 tables to anonymous users

https://abcdefghijklmnopqrst.supabase.co/rest/v1/ returned the API schema using only the public anon key, revealing table names (e.g. profiles, payments, posts, messages, waitlist, admin_settings). Column names that look sensitive are also visible: profiles (email, phone, stripe_customer_id); payments (stripe_customer_id); waitlist (email). Names alone are not a breach, but they map out your data for attackers, so every table needs RLS.

Plain-English fix

Your Supabase table and column names are visible with the public anon key. That alone is normal for Supabase, but it tells attackers exactly where to look, so every table must have RLS and internal tables should not be in the public API.

Supabase SQL editor (paste and run)

-- 1) Tables without Row Level Security (fix every row this returns)
select tablename from pg_tables where schemaname = 'public' and not rowsecurity;
-- 2) Policies that allow everyone (qual = 'true') — review each
select tablename, policyname, roles, cmd, qual from pg_policies where schemaname = 'public' and (qual = 'true' or 'anon' = any(roles));
-- 3) For internal tables the browser never reads:
-- revoke all on table public.<name> from anon, authenticated;

-- Then check from a terminal:
-- curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Prompt for Cursor

The Supabase REST schema (/rest/v1/) lists our tables to anonymous users (profiles, payments, posts, messages, waitlist, admin_settings).
1) Enable RLS on every table in public and add auth.uid()-scoped policies (find gaps with: select tablename from pg_tables where schemaname='public' and not rowsecurity;).
2) Move internal/admin tables to a private schema that is not in "Exposed schemas" (Project Settings > API), or revoke anon access: revoke all on table public.<name> from anon;
3) Write the SQL as a migration file and apply it.

How to check it worked: curl "https://abcdefghijklmnopqrst.supabase.co/rest/v1/?select=*&limit=1" -H "apikey: YOUR_ANON_KEY"   # expect [] or a 401/permission error

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
LOW

Supabase Storage bucket list visible to anonymous users (1)

https://abcdefghijklmnopqrst.supabase.co/storage/v1/bucket returned your bucket names with only the anon key: invoices. Public buckets serve any file to anyone who knows its path.

Plain-English fix

Anyone can see your Supabase Storage bucket list with the public anon key. Remove the storage policy that lets anonymous users SELECT, and store private files under a folder named after the user's id with owner-only policies.

Supabase SQL editor (paste and run)

-- Who can list buckets? Drop any policy that lets anon read storage.buckets:
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'buckets';
-- drop policy "<policy name>" on storage.buckets;
-- Then check object policies (listing files) the same way:
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'objects';

-- Then check from a terminal:
-- curl -X POST "https://abcdefghijklmnopqrst.supabase.co/storage/v1/object/list/invoices" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY" -H "Content-Type: application/json" -d '{"prefix":"","limit":1}'   # expect []

Prompt for Cursor

Supabase Storage bucket metadata can be listed anonymously. Write and apply a migration with this SQL (adjust the folder convention if files are not stored under <user id>/...):

-- Who can list buckets? Drop any policy that lets anon read storage.buckets:
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'buckets';
-- drop policy "<policy name>" on storage.buckets;
-- Then check object policies (listing files) the same way:
select policyname, roles, cmd, qual from pg_policies where schemaname = 'storage' and tablename = 'objects';

Then update upload code to save files at `${user.id}/<filename>`, and use createSignedUrl() for private downloads.

How to check it worked: curl -X POST "https://abcdefghijklmnopqrst.supabase.co/storage/v1/object/list/invoices" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY" -H "Content-Type: application/json" -d '{"prefix":"","limit":1}'   # expect []

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
INFO

Supabase project URL in client

Supabase URL https://abcdefghijklmnopqrst.supabase.co is public by design; ensure RLS protects your tables.

Evidence: https:…e.co

Plain-English fix

Your Supabase URL and anon/publishable key in the browser are normal. They are only safe if Row Level Security is on for every table, so check that.

Supabase SQL editor (paste and run)

-- Tables in the public schema without Row Level Security:
select tablename from pg_tables where schemaname = 'public' and not rowsecurity;
-- For each one: alter table public.<name> enable row level security;  then add policies using (select auth.uid()).

Prompt for Cursor

The Supabase URL + anon/publishable key are public by design. Audit RLS: list tables without RLS (select tablename from pg_tables where schemaname='public' and not rowsecurity;), enable it on each, and add policies scoped to auth.uid(). Confirm no service_role key is in client code.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
INFO

Supabase anon key in client (expected)

Anon/publishable keys are designed for the browser, but only safe if RLS is enabled on every table. Redacted: eyJhbG…real.

Evidence: eyJhbG…real

Plain-English fix

A JWT (signed token) is hard-coded in your client code. If it is anything other than a public anon key, it gives whoever copies it the same access. Check what it is and move privileged tokens server-side.

Prompt for Cursor

A hard-coded JWT is in the client bundle. Decode it (jwt.io or base64 the middle part) and check its role/claims. If it is anything other than a public anon key (e.g. service_role, admin, a long-lived user/session token), remove it from client code, move the calls that need it to a server route, and revoke/rotate it with whoever issued it.

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.
INFO

No security.txt (how to report a vulnerability)

https://your-app.lovable.app/.well-known/security.txt was not found. It is a small text file (RFC 9116) that tells people who find a security bug how to reach you, instead of posting it publicly.

Plain-English fix

Add a /.well-known/security.txt file with a Contact email and an Expires date, so people who find a bug can tell you privately instead of posting it.

Prompt for Cursor

Add a security.txt file (RFC 9116) served at /.well-known/security.txt as text/plain:
Contact: mailto:security@your-domain.com
Expires: <a date under 1 year from today, ISO 8601, e.g. 2027-10-01T00:00:00.000Z>
Preferred-Languages: en
Canonical: https://your-app.lovable.app/.well-known/security.txt
Put it in the static folder that is served as-is (public/.well-known/security.txt for Vite, Next.js and most hosts), make sure the router does not swallow /.well-known/, and redeploy.

How to check it worked: curl -s https://your-app.lovable.app/.well-known/security.txt

Work in this repo: find every affected file, make the smallest safe change, show me the diff, and list the manual steps I still need to do outside the code.

Get yours: free scan

Paste your live URL and see your grade and every biggest risk free in about 30 seconds. The full report like this one is a one-time $29 with a 14-day refund, and a free re-check after you fix.

Run my free scan

No signup. Passive, read-only checks.

What this scan doesn't catch

Passive read-only scan. Not a pentest. Fix prompts are templates written for each finding; review changes before you ship them.