The Supabase misconfiguration that exposed 1.5 million records
The incident followed a pattern that has since become familiar. A founder built a SaaS application using an AI coding tool and Supabase as the backend. The app launched, gained traction, and processed thousands of signups over several months. The problem had been present since day one: the Supabase service role key was set as a NEXT_PUBLIC_environment variable, which caused it to be included in the client-side JavaScript bundle, and Row Level Security was never enabled on any of the database tables. An automated security scanner, run by a researcher who was building an inventory of common Supabase misconfigurations, identified both issues within 48 hours of the researcher targeting the app's domain. The database contained 1.5 million rows of user data across multiple tables. None of it required authentication to read.
The two mistakes compounded each other in a specific way. The Supabase service role key bypasses Row Level Security entirely — it is designed for administrative use from server-side code, and Supabase intentionally gives it unrestricted access to the database for that purpose. Had the anon key been exposed instead of the service role key, and had RLS been enabled, the exposure would have been contained: the anon user has no user ID, so RLS policies requiring auth.uid() = user_id would have returned zero rows. But with the service role key, RLS is irrelevant regardless of whether it is enabled — the key overrides it. And with RLS disabled, even the anon key would have granted full access. Either mistake alone would have been serious. Together, they created a situation where two completely independent defenses were both absent simultaneously.
The access method required no specialized tools or exploits. The researcher used the standard Supabase JavaScript client library, the publicly visible project URL from the app's JavaScript bundle, and the service role key extracted from the same bundle using browser DevTools. The client library's .from("table_name").select("*") method returned every row in every table. The user profile table contained names, email addresses, and hashed passwords. A second table contained usage records linked to user IDs. A third table contained subscription and billing metadata. All of it was accessible to anyone who took the time to open DevTools and read the JavaScript bundle — no login required, no reverse engineering required.
The investigation took two days after the researcher disclosed the findings. The team audited Supabase's access logs and found no evidence of malicious access — the vulnerability had been present for several months, but as far as the logs showed, the researcher was the first person to access the data via the exposed key. This is not an unusual outcome: many such exposures are discovered by researchers or security scanners before active attackers find them, because researchers are specifically looking for these patterns. The founder was fortunate. The outcome could have been different if the vulnerability had been discovered by someone who sold the extracted data rather than disclosing it.
Checking your own Supabase project for the same exposure takes about three minutes. Open your browser DevTools, load your deployed application, and search the Sources tab for your Supabase project URL. Once you find it, look at the environment variables loaded alongside it. If you see both a project URL and a key that contains service_roleor is significantly longer than your anon key, that is your service role key and it is exposed. To check RLS status, open the Supabase dashboard, go to the Table Editor, and look for the RLS toggle on each table — a grey or off state means all access is permitted by default. To check Storage, go to Storage in the Supabase dashboard and look for buckets marked as "Public."
The remediation order matters. First, rotate the service role key immediately — go to Supabase Project Settings and generate a new service role key, then update it in your hosting platform's environment variable system. The old key is invalidated. Second, move the service role key from any NEXT_PUBLIC_ variable to a server-only environment variable and update your code to use it only in server-side contexts. Third, enable RLS on every table using the Table Editor toggle. Fourth, write deny-by-default policies: start with USING (false) for SELECT to confirm the table is locked, then replace it with your actual authorization condition. Fifth, test from the anon perspective to confirm the data is no longer accessible without authentication.