Security Education

9 min

The Supabase security checklist I run before any app goes live

The Supabase security checklist I run before any app goes live

The full Supabase security checklist covering RLS, service_role key exposure, storage permissions, auth configuration, and database functions. Ordered by severity.

The full Supabase security checklist covering RLS, service_role key exposure, storage permissions, auth configuration, and database functions. Ordered by severity.

Supabase makes it genuinely easy to go from idea to working application fast; and after all, that’s the exact point of it. The problem however is that the same defaults that make development fast can also leave a production app wide open if nobody checks them properly prior to launch (and in my experience auditing Supabase based apps, most founders are not checking them).

83% of Supabase database exposures trace back to misconfiguration rather than any flaw in the platform itself. Supabase is secure, however it is the apps that are built on it and often are not, and the gap between these two areas is almost always a handful of configuration decisions that never got made.

That being said, this checklist is what I work through on every Supabase audit, ordered by severity, with links out to the deeper write ups where relevant.


Before anything else: understand the threat model

Supabase exposes your PostgreSQL database directly to client side code via the anon key. this is a key that ships in your JavaScript bundle, and It’s visible to anyone who opens DevTools. it’s located there by design, with the security model being built around it. The anon key is intentionally public, but what isn’t public is your data, with the only thing standing between that key and your data being RLS.

This is the single most important thing to internalize before working through any of the checks below. If RLS isn’t configured, the anon key is effectively a master key to every table it can reach.


Critical

Fix these before anything else. An app with any of these unresolved should not be live with real users.

RLS enabled on every table

RLS is disabled by default on all tables. Tables created through SQL, migrations, or AI coding tools do not get it automatically, only tables created through the Supabase Table Editor dashboard do. Every table in your public schema that contains user data, business data, payment information, or anything that belongs to a specific user needs RLS enabled explicitly.

Check: Dashboard → Table Editor. Any table showing “RLS disabled” is fully readable and writable by anyone with your anon key. You can also run a direct request against your REST endpoint with the anon key and observe what comes back. If you get real data, the table is exposed.

Relevant reading: What actually happens when your Supabase RLS is misconfigured


service_role key not in client-side code

The service_role key bypasses RLS entirely on every table regardless of whatever policies you’ve configured. If it appears in your frontend bundle, this means that every security control you’ve set up becomes completely irrelevant. This key belongs exclusively in server-side code: things like Edge Functions, backend APIs, nothing that runs in the browser.

Check: DevTools → Sources → Search (Ctrl+Shift+F). Search for service_role and your actual key value. If either appears, that’s an immediate critical finding. Rotate the key in the Supabase dashboard before anything else, then audit your query logs for requests that shouldn’t have come from your application.

Relevant reading: Supabase security beyond RLS


RLS policies actually scoped correctly

Enabling RLS and writing a policy are two separate steps, and the policy itself can be misconfigured in a way that’s easy to miss. A policy written as USING (true) grants every row to every user and provides no protection at all. The correct pattern for user-scoped data is USING ((select auth.uid()) = user_id). The select wrapper matters for performance on larger tables: it evaluates once per query rather than once per row.

Check: Dashboard → Database → Security Advisor. this is also worth verifying manually with two separate test accounts, and check: can account A read or modify data that belongs to account B? If yes, the policy is wrong regardless of what the dashboard shows.


High

Serious issues that expand your attack surface significantly.

Storage bucket permissions

Storage buckets have their own access control layer separate from database RLS, and a public bucket means anyone can read every object in it with no authentication. This is appropriate for marketing assets, not for user profile photos, uploaded documents, or anything private. The other failure mode is a private bucket with no policies configured, which denies everything and usually means storage isn’t working as the founder intended.

Check: Try accessing a storage URL without any authentication. If you get the file back, the bucket is public. Configure RLS policies on storage.objects scoped to authenticated users and ownership for any bucket that shouldn’t be publicly readable.


Database functions callable without authentication

Supabase RPC functions can be called from the frontend and, depending on how they’re defined, can bypass RLS entirely. A function set to SECURITY DEFINER runs with the permissions of the function owner rather than the calling user. AI generated Supabase code has a consistent habit of creating RPC functions without authentication checks.

Check: Any function touching sensitive data should raise an exception immediately if auth.uid() is null. Try calling your RPC functions without an Authorization header and observe what comes back.


anon key in bundle with no RLS

The anon key in your frontend bundle is expected and by design, however it becomes a critical finding when combined with tables that don’t have RLS configured. The key itself isn’t the problem, however the missing policies are. That said, confirming the key is present and understanding which tables it can reach is a useful audit step regardless.

Check: DevTools → Sources → Search for anon and eyJ. Note what’s present, then cross-reference against your table RLS status.


Medium

Worth fixing before launch, non-negotiable for apps handling sensitive data.

Auth configuration

Email confirmation is off by default on new Supabase projects, and users can sign up and access the application without verifying ownership of their email address. For most production apps, this is worth enabling prior to launch. Also, I would also recommend checking your password reset flow: if it returns a distinct message for email addresses that don’t exist versus ones that do, that’s email enumeration.

Check: Dashboard → Authentication → Settings. Enable email confirmation. Then enter an unregistered email on your password reset page and observe the response. A generic message regardless of whether the address exists is correct. A specific “no account found” message is a finding.


Rate limiting on auth endpoints

Supabase handles rate limiting at the platform level but it’s worth verifying your configuration hasn’t overridden it. Without it, automated tools can cycle through password lists against any account on your platform overnight with no resistance.

Check: Attempt 15 or more rapid login attempts with incorrect credentials. If nothing slows down or blocks you, rate limiting isn’t working as expected.


Security headers

These live at the hosting layer rather than inside Supabase itself, but they’re part of the same pre-launch picture. Missing CSP, X-Frame-Options, and Permissions-Policy are consistently among the most overlooked items in AI generated apps.

Check: securityheaders.com (a grade of B or above is ideal).

Relevant reading: Security headers explained


The Security Advisor is worth running

Supabase ships a built-in database linter called Splinter (which should accessible under Dashboard → Database → Security Advisor). It runs automated checks for tables with RLS disabled, overly permissive policies, unsafe views, and exposed functions. It’s not a substitute for manual testing but it catches a meaningful portion of configuration-level issues and costs nothing to run. Worth making it part of your routine before any significant deployment.

Organisation owners also receive weekly security emails from Supabase summarising any findings the advisor has detected. Thus, being in-built it’s more than worth making sure those are going to someone who will actually act on them.

If you want someone to work through all of this against your actual build with adversarial intent rather than just a configuration review, that’s what a HollowByte pre-launch audit covers.

Back to blog

(function() { function applyMainRole() { var hero = document.getElementById('hero'); if (!hero) return false; var node = hero; while (node.parentElement && node.parentElement !== document.body) { node = node.parentElement; } if (node && node.parentElement === document.body) { node.setAttribute('role', 'main'); return true; } return false; } if (applyMainRole()) return; var attempts = 0; var interval = setInterval(function() { attempts++; if (applyMainRole() || attempts > 20) { clearInterval(interval); } }, 250); })();