Exactly what an anon key can do before you write a policy
Every Supabase frontend ships with a key visible in view-source, and the recurring worry in every community thread is whether that is safe. The complete answer is a list: the anon key can do exactly what the database grants the anon role, and nothing more. Before you write a single policy, that role inherits broad grants — so the list below is what the key reaches in a fresh project.
This inventory is reference material rather than a product feature. RowShield does not automate capability enumeration, though its probe does test the sharpest item on the list directly, as noted at the end.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
The key is an identity, not a permission slip
The anon key exists to tell PostgREST "this caller has not signed in". It travels openly because it must — every unauthenticated browser session presents it — and it carries no power of its own. Power comes from two database-side mechanisms: the GRANTs held by the anon role, which are broad in a stock project, and Row Level Security, which bounds those grants per table once enabled. Reading the key out of a bundle tells an attacker nothing they lacked; reading a table whose RLS is missing tells them everything.
That division of labour is why hiding the key is never the fix. Any obfuscation merely delays discovery by minutes, while the underlying question — what does the anon role reach — remains exactly as configured.
With RLS off, the full table
On any PostgREST-exposed table without Row Level Security, the anon key reads every row, sorts and filters them with query parameters, and — grants permitting — inserts, updates and deletes too. A curl one-liner is the entire attack, and it succeeds silently with status 200. This is the single most common shape of a reported Supabase leak, and it looks identical to normal traffic in your logs:
curl "https://your-project-ref.supabase.co/rest/v1/profiles?select=*" \ -H "apikey: eyJhbGciOi…REDACTED"
With RLS on but no policies, everything denied
Enable RLS and stop there, and the picture inverts completely: Postgres denies every row of that table to the anon role, along with every other non-owner role. Data becomes unreachable rather than exposed — lists render empty, forms fail quietly, and the application breaks without an error anywhere. Both states stem from the same skipped step, writing the policies, and they fail in opposite directions, which is why tools report them as separate findings.
The working middle is the ordinary configuration: RLS enabled, policies scoping rows to their owners, the anon key still travelling in every request but reaching only what those policies admit.
Beyond tables: storage, realtime and auth endpoints
Tables are the headline, not the whole list. Storage access flows through policies on storage.objects, so a fresh project's buckets are only as closed as those policies; Realtime change delivery honours the same SELECT policies governing table reads, so an over-broad read policy leaks through subscriptions as well as through fetches. Auth endpoints — sign-up, sign-in, password reset — are intentionally available to the anon key, since signing in is its purpose. Edge functions receive the caller's JWT and enforce their own checks, so a function that skips verification extends the anon role's reach by accident. Each surface reduces to the same rule: somewhere stands a policy or a guardrail, or the anon role walks straight through.
Testing the sharp edge automatically
RowShield does not automate this page's full inventory — capability enumeration across every surface is a manual discipline. What its probe does answer, with a single GET and no account, is the highest-stakes item: whether the anon key can read each exposed table right now. Findings name the table and column count, never row contents; the probe refuses private network addresses before connecting; and scheduled scans catch the moment a new table ships without policies. RowShield is an independent product, not affiliated with or endorsed by Supabase. Try it at rowshield.dev/audit — paste a project URL, nothing else required.
Frequently asked
- Should I try to hide or rotate my anon key?
- No. It is designed to be public, ships in every bundle by necessity, and rotating it accomplishes nothing because the replacement publishes identically. Safety comes from policies bounding what the key can do, not from concealment.
- Is the anon key the same thing as the service_role key?
- They are opposites. The anon role holds no special privileges and is bounded entirely by RLS; service_role bypasses RLS altogether and must never leave a server. One is printed in documentation examples, the other ends careers.
- Can the anon key write data, not just read it?
- If grants allow it and no INSERT policy restricts it, yes — which is why write policies need WITH CHECK clauses naming who may be impersonated in a row. Unchecked writes let anyone attribute records to other users, even when reads are locked down.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit