Your API surface is bigger than you think

Supabase gives you a REST API over your schema, a Realtime socket, Edge Functions, and Storage — all reachable from the public internet the moment your project exists. Every one of them is a way into your data, and they fail differently. Here are the mistakes that actually leak data, and the log signature each one leaves.

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 the service key bypasses Row Level Security entirely, its exposure invalidates 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

A function deployed with JWT verification off accepts calls from anyone. That is correct for a Stripe webhook and dangerous for anything touching user data — particularly since functions frequently hold the service key in their environment. The combination gives an anonymous caller a fully privileged path into your database through a URL that is easy to guess.

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 RLS. Broadcast and presence do not — they are their own authorization surface. A channel that any client can join will deliver every message published to it. 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

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.

Frequently asked questions

Is it safe to put the Supabase anon key in a frontend deployed to Vercel?
Yes — the anon key is designed to be public and belongs in the browser bundle. The service_role key is not, and the mistake that matters is prefixing it so a bundler ships it. Anything named NEXT_PUBLIC_* is compiled into client JavaScript; a service key with that prefix is published to every visitor.
Are Supabase Edge Functions authenticated by default?
A function deployed without verify_jwt will accept unauthenticated requests. That is the right default for a webhook receiver and a serious hole for anything that reads user data. Functions also commonly hold the service key in their environment, so an unauthenticated function with a service key is an unauthenticated path around every RLS policy you wrote.
Does Row Level Security apply to Realtime subscriptions?
For Postgres Changes, yes — Supabase applies RLS to the rows a subscriber receives. But Realtime's broadcast and presence features are a separate channel with their own authorization, and a channel that anyone can subscribe to will happily deliver whatever is published to it, regardless of what your table policies say.
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.