I Ran a Supabase Security Audit From My Phone — Here's What I Found
Checking whether the database backing a public directory site was actually as secure as I assumed.
*Checking whether the database backing a public directory site was actually as secure as I assumed.*
It's easy to build features on top of Supabase and never step back to specifically ask: if someone hit this database directly, bypassing the app entirely, what could they actually do? That question is what this audit set out to answer for ShabelleHub.
Why a Dedicated Audit, Separate From Feature Work
Security checks that happen incidentally, as a side effect of building a feature, tend to only cover the exact path that feature uses. A dedicated audit means going table by table, policy by policy, and asking the adversarial question directly rather than the feature-building question of "does this work for the intended use case."
What Got Checked
Row Level Security (RLS) policies on every public-facing table — confirming that anonymous users could only perform the operations they were meant to (for example, inserting into tool_events for tracking, but not reading other users' event rows, and not being able to update or delete arbitrary rows).
SECURITY DEFINER functions — Postgres functions that run with elevated privileges regardless of who calls them. These are powerful and, if misconfigured, can quietly bypass RLS entirely, since the function executes as its owner rather than as the calling user. Each one needed to be checked individually to confirm it wasn't granting more access than intended.
Anonymous vs. authenticated access boundaries — making sure that operations meant to require authentication weren't accidentally reachable by the anonymous role, and vice versa, that legitimate anonymous operations (like submitting a tool review) weren't over-restricted in a way that silently broke functionality.
The Result
The audit confirmed that the RLS policies and SECURITY DEFINER functions were implemented correctly — not a foregone conclusion, but a genuinely useful outcome to have verified explicitly rather than assumed. Passing an audit isn't the interesting part of a security story; the interesting part is that it *could* have gone the other way, and the only way to know is to actually check.
Doing This From a Phone
Supabase's dashboard is fully usable on mobile, including the Table Editor and policy views, but auditing multiple tables and their associated policies means a lot of navigating between screens that would be simultaneously visible on a desktop. The practical approach was methodical rather than exploratory: a written checklist of every table and every policy to review, worked through one at a time, rather than relying on memory of what had already been checked across app-switches.
Making This a Recurring Habit, Not a One-Time Event
A security audit performed once, at a single point in time, only covers the schema as it existed on that day. Every new table, every new SECURITY DEFINER function, every policy adjustment made afterward is, by definition, unaudited until it's specifically reviewed. The intention going forward is treating this as a recurring checklist item tied to any schema change of consequence, rather than a one-time project that gets marked complete and forgotten — a lightweight version of the same audit, re-run whenever a new table or function touching public-facing data gets added, rather than waiting for the next dedicated audit cycle to catch it.
Why This Matters for a Directory Site
ShabelleHub accepts public interactions — searches, clicks, potentially reviews — from anonymous users by design. That's exactly the kind of surface area where a misconfigured policy turns into a real vulnerability, not a theoretical one. A publicly writable table with a bad policy isn't a hypothetical risk; it's an open door.
The Checklist, Roughly
Rather than reconstruct the exact checklist here, it's more useful to describe its shape: for every table, list what operations (select, insert, update, delete) each role (anonymous, authenticated, service) should be able to perform, write that down *before* looking at the actual policies, and only then compare the written expectation against what the policies actually enforce. Doing the comparison in that order — expectation first, reality second — matters, because reading policies cold and asking "does this look okay" is much more likely to rubber-stamp something that merely looks plausible than actually catching a gap between intent and implementation.
One Thing That Almost Got Missed
A near-miss during the audit involved a table that had a correct, narrowly-scoped RLS policy for direct access — but was also referenced by a SECURITY DEFINER function whose own internal query didn't respect the same narrow scope, because functions running with elevated privileges aren't automatically bound by the RLS policies on the tables they touch. The table itself would have passed a policy-only review; the function accessing it needed a separate, deliberate check to confirm it wasn't quietly exposing more than the table's own policy intended. That near-miss is a good illustration of why auditing tables and auditing functions have to be treated as genuinely separate steps, not one check that happens to cover both.
Takeaway
If your Supabase project accepts any writes from anonymous or lightly-authenticated users, a dedicated audit — table by table, function by function — is worth doing deliberately, separate from feature development, even if (especially if) you're fairly confident everything is already configured correctly.
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.