Supabase API security: protect every public route to your data
Supabase API security is a chain: safe keys, narrow Postgres grants, Row Level Security, authenticated Edge Functions, private Realtime channels, and monitoring after deployment. A strong control on one surface does not repair a public route on another.
The Supabase Data API is secured by two Postgres controls: grants decide which objects and operations a role can reach, and Row Level Security decides which rows it can access. Configure both for every exposed table, view, and function.
The public or legacy anon key belongs in client code; secret and legacy service-role keys do not. Edge Functions keep JWT verification on by default, while public webhooks need their own signature check. Realtime Broadcast and Presence should use private channels with authorization policies.
A practical Supabase API security checklist
Start with the official Data API security model: grants control which tables, views, functions, and operations a role may reach; RLS controls which rows that role may use. Then secure each additional public surface separately.
- Data API: revoke unnecessary grants and enable RLS on every object in an exposed schema.
- Keys: keep secret and legacy service-role keys on trusted servers only.
- Edge Functions: leave JWT verification on for user calls; verify signatures on public webhooks.
- Realtime: use private production channels and policies on
realtime.messages. - Storage: use private buckets for user files and policies that scope objects to the owner or tenant.
- Monitoring: alert on privileged browser requests, unusual bulk reads, repeated denials, and request floods.
The service key in a browser bundle
This is the highest-severity mistake on the list, and it is almost always an environment-variable accident: a service key named with a NEXT_PUBLIC_ prefix, or passed into a component that runs on the client. Bundlers inline those values at build time, so the key ships to every visitor and is trivially readable in devtools.
Because a privileged key can bypass Row Level Security, its exposure can invalidate every policy at once. The tell in your logs is unmistakable: service_role requests carrying a browser user-agent and an Origin header. Real server-to-server traffic has neither.
Edge Functions without JWT verification
Supabase enables JWT verification by default. When you turn it off for a public webhook, the platform allows the request to reach your code without a user JWT. The handler must then verify the provider's signature or another credential before doing privileged work. Otherwise an anonymous caller may reach a database client that bypasses RLS through a URL that is easy to discover.
Watch for functions whose invocation volume jumps without a corresponding change in app traffic, and for elevated 5xx rates after a deploy, which frequently signal someone probing arguments the function was never designed to receive.
Realtime channels with open subscriptions
Postgres Changes respect table RLS. Broadcast and Presence use Realtime Authorization with policies on realtime.messages. A public channel can be joined by any client, while a private channel checks the caller's permissions. It is also worth watching connection rates: a single address opening thousands of sockets is either a bug in your reconnect logic or someone exhausting your connection limit, and the two look identical until you check the source.
The REST surface
PostgREST exposes your tables directly, which is powerful and unforgiving. Selecting with * over a table whose policies are broader than you thought returns more columns than you intended. Bulk reads — thousands of rows in one query from an unusual source — are worth flagging even when technically permitted, because permitted mass extraction is what most data breaches actually consist of.
What to monitor across all four
- service_role from a browser context — critical; rotate the key immediately.
- Unauthenticated function invocations — especially any function holding privileged keys.
- Anonymous request bursts — enumeration and scraping against the REST surface.
- Realtime connection floods — resource exhaustion, or a broken client.
- 4xx and 5xx rate changes after a deploy — the shape of someone probing a new endpoint.
How Defencecore catches it
Defencecore ingests API gateway, Edge Function, Realtime, and Storage logs alongside auth and Postgres, and runs its rules across all of them together — which matters, because the interesting incidents span surfaces. A service key seen in the browser and a bulk read from the same session are one story, not two alerts. Each incident arrives with its evidence, a severity, and a recommended first action.
This is where continuous monitoring differs from an API checklist. The checklist proves the intended configuration today. Defencecore watches what the API does tomorrow: which roles appear, where requests come from, whether denials spike, and whether one identity starts reading or calling far more than normal.
Continue with the guide to Supabase anon-key exposure or compare Defencecore with manual Supabase log review.
Frequently asked questions
- Is it safe to put the Supabase anon key in a frontend deployed to Vercel?
- Yes. The legacy anon key and its newer publishable-key replacement are designed for public clients. A secret or legacy service_role key is not. The dangerous mistake is giving a privileged key a NEXT_PUBLIC_ or VITE_ prefix, which compiles it into client JavaScript and publishes it to every visitor.
- Are Supabase Edge Functions authenticated by default?
- Supabase enables verify_jwt by default. If you explicitly disable it, the platform no longer requires a valid user JWT before the handler runs. That can be correct for a webhook, but the handler must then verify the webhook signature or another credential itself. A public function using a privileged database client can become a path around RLS.
- Does Row Level Security apply to Realtime subscriptions?
- Postgres Changes respects the RLS policies on the source table. Broadcast and Presence use Realtime Authorization instead: production channels should be private, and RLS policies on realtime.messages decide who may send or receive. A public channel allows any client to subscribe, send, and receive messages.
- What is the fastest way to tell if my service key has leaked?
- Look for it arriving with a browser user-agent or a website Origin header in your API logs. Server-to-server calls do not carry those. One request with service_role, a Mozilla user-agent, and an Origin from your own marketing site is effectively proof that the key is in a client bundle, and the key should be rotated immediately.