Supabase Row Level Security (RLS) and Firebase Security Rules are the locks that decide who can read and change each record in your database. Both platforms let the browser or mobile app talk to the database directly, using a public key that anyone can see. So the database itself has to check every request. In Supabase you switch on RLS for every table and write a policy such as "a user can only see rows where user_id is their own ID". In Firebase you write rules that compare request.auth.uid with the owner of each document. If RLS is off or the rules say if true, anyone who opens your site can copy or delete your data.
This guide is for founders and developers whose app was built quickly, often with Lovable, Bolt, Replit, Cursor or another AI tool, and for business owners who want to know how exposed they are. It explains why the key isn't the problem, what has gone wrong for real companies, and how to write and test the rules on both platforms.
Why the browser talks to your database directly
In a classic web app, the browser talks to your server and only the server talks to the database. Your server code decides what each user may see. Supabase and Firebase remove that middle layer to save time. The app in the browser sends its query straight to the database API, and you don't have to write or host a backend for every screen.
That speed is why AI app builders use them, and it means the check that used to live in your server code now has to live in the database. Firebase's own documentation calls its rules the only safeguard between your data and a malicious user.
The public key isn't a secret
Open the developer tools on any Supabase or Firebase app and you'll find the project URL and an API key in the JavaScript. That's normal. Supabase's publishable key (sb_publishable_..., called the anon key in older projects) and the Firebase config apiKey identify your project. They're meant to ship in the app. Firebase says plainly that its API keys aren't used to control access to your data.
Hiding or obfuscating the key changes nothing. Anyone can copy it and query your database from their own laptop, and what they get back depends only on your RLS policies or your rules.
The keys that are secret are Supabase's secret key (sb_secret_...) and the older service_role key, plus Firebase service account credentials used by the Admin SDK. These skip RLS and rules entirely. They belong on a server, in an Edge Function or Cloud Function, and never in the browser, a mobile app or a Git repository. Supabase now rejects secret keys sent from a browser, but a leaked key still works from any other tool, so a key that has ever been exposed must be rotated.

The public key is the same in both cases. The rules decide what it opens.
What happens when RLS is off or rules are open
These are public cases and published research. They show the same mistake on both platforms.
- Lovable apps, 2025. A researcher scanned 1,645 apps from Lovable's public showcase and found 170 whose Supabase databases could be read or changed without logging in, because RLS policies were missing or too loose. It was recorded as CVE-2025-48757. Lovable's position was that each app owner is responsible for their own data.
- Moltbook, 2026. Wiz researchers found that this social network for AI agents had a Supabase key in its front end, as usual, and no RLS policies behind it. The key gave full read and write access to the production database, including about 1.5 million API tokens, tens of thousands of email addresses and private messages. The team fixed it within hours of being told.
- Firebase sites, 2024. Three researchers scanned more than five million domains and found 916 websites with Firebase rules that were missing or wrongly set. Together they exposed about 125 million user records, including names, emails, phone numbers, billing details and passwords stored in plain text. Only about a quarter of the companies fixed the problem after being warned.
- Vibe-coded apps, 2025. Escape, a security company, scanned about 5,600 public apps built on AI platforms and reported more than 2,000 high-impact vulnerabilities, over 400 exposed secrets and 175 cases of exposed personal data, including medical records and bank account numbers.
None of this needed hacking skill. For a business, the result is a data breach you may have to report to regulators and customers, and a loss of trust that's hard to win back. Our article on the security risks of AI-generated code covers the other problems that usually come with it.
Supabase RLS: lock down each table
Step 1: switch RLS on for every table
Tables created with SQL, migrations or AI tools don't always have RLS switched on. Run this in the SQL editor to list the tables in the public schema that are still open:
select tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Then switch it on for each one:
alter table public.invoices enable row level security;
Once RLS is on and there are no policies, the public key can't read or write anything in that table. That's the safe starting point. You then add one policy for each thing a user is allowed to do. The Security Advisor in the Supabase dashboard also flags tables without RLS (lint 0013) and policies that exist on a table where RLS is still off (lint 0007).
Step 2: policies for each user's own rows
Say each row in invoices has a user_id column holding the ID of the user who owns it. These four policies let signed-in users work only with their own rows. auth.uid() returns the ID of the user making the request, and wrapping it in (select ...) lets Postgres work it out once per query instead of once per row, as Supabase recommends.
create policy "Users can read their own invoices"
on public.invoices for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users can create their own invoices"
on public.invoices for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Users can update their own invoices"
on public.invoices for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Users can delete their own invoices"
on public.invoices for delete
to authenticated
using ( (select auth.uid()) = user_id );
create index on public.invoices (user_id);
using checks the rows that already exist. with check checks the row being written. The update policy needs both, or a user could change user_id and hand a row to someone else. to authenticated means visitors who aren't signed in get nothing. The index keeps the check fast as the table grows.
Step 3: policies for teams and companies
Business apps usually share records within a team or company. Keep a team_members table that links users to teams, then let users reach rows of the teams they belong to:
create policy "Team members can read team projects"
on public.projects for select
to authenticated
using (
team_id in (
select team_id from public.team_members
where user_id = (select auth.uid())
)
);
Write matching policies for insert, update and delete, and decide who may add people to a team. The team_members table needs RLS too. If its own policy looks up team_members, Postgres reports infinite recursion. The usual answer is a small security definer function, in a schema the API doesn't expose, that returns the user's teams.
Never base a permission on user_metadata in the login token, because users can change their own metadata. Store roles in a table that only admins or the server can write. Views also skip RLS by default. On Postgres 15 and later, create them with (security_invoker = true) so they respect the policies of the tables underneath.
Step 4: storage buckets
Files in Supabase Storage are rows in storage.objects, so the same rules apply. Make buckets private unless every file in them really is public, and give each user 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 = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Users read their own folder"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
Any job that must see everything, such as a nightly report or a payment webhook, runs on the server with the secret key. If an AI tool put the service_role key in your front-end code to make an error go away, treat it as leaked. Rotate it and move that logic to an Edge Function.
Firebase Security Rules: Firestore and Storage
Get out of test mode
When you create a Firestore database in test mode, the console writes rules that let anyone read and write until a date about 30 days ahead:
allow read, write: if request.time < timestamp.date(2026, 12, 7);
Until that date your data is open to the internet. On that date the app stops working, and many people "fix" it by moving the date forward or changing the line to allow read, write: if true;. Firebase says never to use that in production. Checking only that a user is logged in (if request.auth != null) isn't enough either, because anyone can create an account in your app and then read everyone's data.
Owner checks and validation in Firestore
Here each note stores the ID of its owner in ownerId. resource.data is the document as it is now, and request.resource.data is the document as it would be after the write.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
allow read, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.ownerId == request.auth.uid
&& request.resource.data.keys().hasOnly(['ownerId', 'title', 'body'])
&& request.resource.data.title is string
&& request.resource.data.title.size() <= 200;
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['title', 'body']);
}
}
}
The create rule stops users from writing notes in someone else's name or adding fields you didn't plan for, such as isAdmin. The update rule lets them change the title and body but never the owner. Rules aren't filters, so your app's query must ask only for the user's own notes (where('ownerId', '==', uid)). A query that could return other people's notes is refused as a whole.
For teams, keep members in a subcollection and check it with exists(). Each lookup counts as a document read on your bill.
match /teams/{teamId}/projects/{projectId} {
allow read, write: if request.auth != null
&& exists(/databases/$(database)/documents/teams/$(teamId)/members/$(request.auth.uid));
}
Cloud Storage rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{fileName} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null && request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
This keeps each user's files private and limits uploads to images under 5 MB, so nobody can fill your bucket at your expense. The Admin SDK on your server skips these rules, so keep its credentials out of the app. Firebase App Check adds a layer by blocking requests that don't come from your real app, but it doesn't replace the rules.
How to test your rules
Test every policy before you rely on it, signed in as three different people: a visitor who isn't signed in, an ordinary user, and a second user who must not see the first user's data.
Testing Supabase
Start with what an outsider sees. Take the publishable key from your own site and query a table from a terminal. With RLS working, you should get an empty list or an error, never your data. (With a legacy anon key, also send it as Authorization: Bearer.)
curl "https://YOUR-PROJECT.supabase.co/rest/v1/invoices?select=*" \
-H "apikey: YOUR_PUBLISHABLE_KEY"
Then check what a signed-in user sees. The SQL editor runs as an admin and sees every row, which makes a broken policy look fine. Switch to a user's role inside a transaction instead, or use the role selector in the dashboard:
begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "ID-OF-TEST-USER-A", "role": "authenticated"}';
select count(*) from public.invoices; -- only user A's invoices
rollback;
For a lasting safety net, write these checks as pgTAP tests and run them with supabase test db before every release, so a new table or a change from an AI tool can't quietly open the data again.
Testing Firebase
The Rules Playground in the Firebase console (Firestore or Storage, then Rules) lets you simulate a read or write as a signed-in user or a visitor before you publish. For repeatable tests, run the Firebase Local Emulator Suite (firebase emulators:start) and write unit tests with @firebase/rules-unit-testing:
const alice = testEnv.authenticatedContext('alice').firestore();
const bob = testEnv.authenticatedContext('bob').firestore();
const visitor = testEnv.unauthenticatedContext().firestore();
await assertSucceeds(getDoc(doc(alice, 'notes/alice-note')));
await assertFails(getDoc(doc(bob, 'notes/alice-note')));
await assertFails(getDoc(doc(visitor, 'notes/alice-note')));
Run them in your build pipeline, so a rules change that opens data fails before it reaches users.
Checklist before you launch

Run these again after every big change, especially one made by an AI tool.
- RLS is on for every table in exposed schemas, and the Security Advisor shows no errors.
- Every Firebase database and bucket has real rules, with no
if trueand no test-mode date. - Each policy or rule checks ownership or team membership, not only that a user is signed in.
- Updates can't change the owner, and writes can't add fields such as roles or prices.
- No secret key,
service_rolekey or Admin SDK credential is in the front end or the repository. Any that ever were have been rotated. - Buckets are private unless every file is meant to be public, with per-user folders and size limits.
- Views use
security_invoker, and roles live in tables users can't edit. - You've tested as a visitor, user A and user B, and the tests run before each release.
If you'd like a quick first read on your app as a whole, our free go-live check asks ten questions in two minutes. For more on building safely with AI tools, see our vibe coding guide.
For business owners: what to ask your developer
You don't need to read SQL to know where you stand. Ask whoever built your app four questions. Is Row Level Security on for every Supabase table, or are there real rules on every Firebase database and bucket? Has anyone tested that one customer can't see another customer's data? Where are the secret keys kept? Who checks this again when the app changes? If the answers are vague, treat your data as exposed until someone shows you otherwise.
Frequently asked questions
What is Row Level Security in Supabase?
Row Level Security is a Postgres feature that Supabase uses to decide, row by row, who can read, add, change or delete data. You switch it on for a table and write policies, for example "users can only see rows where user_id matches their own ID". Every request through Supabase's API is checked against those policies.
Is it safe to expose the Supabase anon key or the Firebase API key?
Yes, as long as RLS or your security rules are correct. These keys only identify your project and are meant to be in the app. The keys you must keep secret are the Supabase secret or service_role key and Firebase service account credentials, because they skip all rules.
What happens if I enable RLS without any policies?
The table is closed to the public and signed-in users. Nobody can read or write it through the API until you add policies, and your app may show empty screens. Server code using the secret key still works. That's safer than leaving RLS off, so switch it on first and add policies one by one.
Does the Supabase service_role key bypass RLS?
Yes. The service_role key and the newer secret keys skip RLS completely, so use them only in server code such as Edge Functions or your backend. If one has ever appeared in your front end or a public repository, rotate it.
How long does Firebase test mode last?
Test mode rules allow anyone to read and write until a date set about 30 days after you create the database. After that, all requests are refused. Replace them with proper rules before you add real data, rather than moving the date forward.
Get a second pair of eyes on your app
If your app was built fast and now holds real customer data, it's worth having someone outside the build check it. Our AI code assessment, from $1,000, reviews an app's security, code quality and readiness for production, and gives you a report of what to fix first. To talk about your app, book a free 30-minute call.