Home › Blog › Supabase · Guide
Supabase Row Level Security, Explained for AI-Built Apps
Row Level Security (RLS) is a built‑in PostgreSQL feature that lets each row in a Supabase table decide who can see or change it. Without RLS, anyone who knows a project’s public API key can request every row, which is a serious privacy risk for apps built by AI coding tools.
What Row Level Security actually does
RLS works like a gatekeeper inside the database. When it is turned on for a table, every SELECT, INSERT, UPDATE, or DELETE operation must pass a rule that explicitly says who may perform that action on each row. If no rule exists, the database blocks the request for both anonymous and logged‑in users, effectively making the table invisible to the outside world. This behavior is independent of any application code, so it protects data even if the front‑end logic is flawed.
Because Supabase automatically creates a REST endpoint for each table in the public schema, the gatekeeper effect of RLS is especially important. The endpoint is reachable with the public anon key, which is intended for unauthenticated visitors. Without RLS, that key grants read access to every row, allowing anyone to scrape user profiles, API tokens, or other sensitive records simply by calling the endpoint.
What happens when RLS is missing
When a table lacks RLS, the public anon role can query the REST endpoint and receive a full list of rows. This exposure happens without any authentication, meaning a malicious script or a curious browser can pull the data with a single HTTP request.
Turning RLS on for a table
Enabling RLS is a single command in the Supabase SQL editor. Once the command runs, the table immediately stops exposing rows to the anon role until policies are added. This step does not affect existing data; it only changes how future queries are evaluated. After enabling RLS, the dashboard will show a clear indicator next to the table name, confirming that the protection is active.
After RLS is enabled, you must create policies that describe legitimate access patterns. A typical pattern for a user profile table is to allow a logged‑in user to read and update only the row that matches their own user identifier. Each policy targets a specific operation—select, insert, update, or delete—and includes a condition that checks the authenticated user’s ID against the row’s owner field. Policies are additive, so you can start with read‑only access and expand as needed.
Quick ways to test that RLS is working
The simplest test is to call the table’s REST endpoint using only the public anon key and no user session. A properly protected table will either return an empty array or produce an error indicating that no rows are visible. If you see actual data in the response, RLS is not active or the policies are too permissive.
A second test involves signing in as a regular user and attempting to read another user’s row. The request should return nothing, confirming that the policy correctly isolates rows by user ID. Performing both tests from a browser console or a lightweight HTTP client gives confidence that the database is enforcing the intended boundaries before the code goes live.
Common pitfalls and how to avoid them
One frequent mistake is storing the service_role key in client‑side code. That key bypasses RLS entirely, rendering any row‑level checks ineffective. Keep the service_role key on a trusted backend server and never expose it in JavaScript, mobile bundles, or public repositories. Instead, use the anon key for public requests and the authenticated key for logged‑in users, which respects RLS policies.
Tables generated by AI coding agents often use the API path, which does not enable RLS by default. After any automated table creation, immediately verify the RLS toggle in the dashboard and add the appropriate policies. Regularly audit the list of tables in the public schema to ensure none slip through without protection.
Copy-paste reference
alter table public.profiles enable row level security;create policy "Users can read their own profile"
on public.profiles for select
to authenticated
using (auth.uid() = id);create policy "Users can update their own profile"
on public.profiles for update
to authenticated
using (auth.uid() = id)
with check (auth.uid() = id);What the documented sources report
UpGuard gathered about 300,000 domains showing signs of Supabase usage and identified 16,326 databases exposing readable tables. No authentication or exploit was needed to read them. Over half of those databases had indicators of personal data (PII); smaller shares included passwords or authentication tokens. UpGuard's explanation: tables created programmatically through the API, which is how coding agents interact with Supabase, do not enable Row Level Security (RLS) by default, while tables created in the Supabase Table Editor UI now do; and public API keys were shared in client-side code and used as if they were secret keys. UpGuard's reading is that these sites are created by AI coding agents while the humans behind them are unaware of the configuration; it did not state what share of the 16,326 came from each cause.
Source: UpGuard, “Everything Everywhere: Systemic Data Exposure in Supabase Apps” (Sept 25, 2026)
TechCrunch reported on September 25, 2026 that Supabase's CISO, Bil Harmer, said Supabase projects are secure by default and that security is a shared responsibility: customers control how their own projects are configured.
Security researcher Matt Palmer's scan, completed on March 21, 2025, identified 303 endpoints across 170 of 1,645 analyzed Lovable projects (about 10.3%) with inadequate Row Level Security (RLS) settings. Depending on the app, exposed data could include personal information from users tables, API keys and access tokens for third-party services, and financial data such as transaction and subscription details, including the ability to modify payment status. The root cause, in the researcher's words, is the failure to enforce or maintain secure default RLS configurations for user projects. Timeline: discovered March 20, 2025; vendor notified March 21; vendor acknowledged March 24; CVE-2025-48757 published and publicly disclosed May 29, 2025.
Common questions
Do I need RLS for tables that only store public content?
That way you retain control if the table’s purpose changes later.
Can I test RLS without writing code?
Use a browser’s address bar or a simple HTTP client to request the REST endpoint with the anon key. The response should be empty or an error if RLS is active, providing a quick sanity check.
What role should I use for server‑side operations that need full access?
Use the service_role key only on a secure backend server. It bypasses RLS, so it must never appear in client‑side code or public repositories.