Blog · October 21, 2026 · Updated October 24, 2026 · 7 min read

Is your Supabase Edge Function open to anyone? How to check

AI tools often switch off JWT checks so an Edge Function 'just works'. How to tell if strangers can call yours, and how to lock it down.

Your app sends emails, charges cards, talks to an AI model or deletes accounts. Behind that button there is probably a small server function. If you use Supabase, it is called an Edge Function, and your AI tool wrote it for you.

Here is the catch: a deployed Edge Function lives at a public web address. Whether a stranger can call it depends on a few lines of setup you may never have looked at. When a function gave a "401 Unauthorized" error during building, a very common quick fix is to turn the login check off. The error goes away, the app works, and nobody notices the lock is gone.

This guide shows how to check your functions in about ten minutes and how to fix them.

Why this matters

Functions are where apps do the powerful things: send messages, change data, use paid services, read every user's rows. If a function does something powerful and does not check who is asking, the "who" can be anyone on the internet.

Access control is also the biggest category of web weakness. In the OWASP Top 10 (2021), Broken Access Control is number one: 94% of the applications OWASP's data covered were tested for it, with over 318,000 recorded occurrences. One of the examples OWASP lists is "force browsing to authenticated pages as an unauthenticated user", which is exactly what happens when a function that should need a login does not.

Two locks, not one

According to the Supabase docs on authorization headers, every request to an Edge Function goes through two layers:

  1. A platform check called verify_jwt. It runs before your code. When it is on (the default), a request without a valid user token is rejected with a 401 and your code never runs.
  2. Your own code. After the request gets through, your code decides what to do with whoever called.

Both matter, and each has a trap.

Trap 1: verify_jwt switched off

The docs say you turn verify_jwt off for functions that are called without a user token, such as webhooks from Stripe or GitHub, or service-to-service calls. That is legitimate. The problem is a function meant for your logged-in users that someone switched off "to fix the 401", and that never checks login in its own code. The Supabase security docs put it directly: turning it off during development "removes the platform's check, leaving withSupabase as the only thing that verifies the caller." If your code does not do that check, nothing does.

Trap 2: verify_jwt on, but not what you think

This one surprises people. The docs warn that, for migration reasons, verify_jwt also accepts Supabase's publishable and secret keys. In their words: "The check alone doesn't authenticate a caller that sends only an API key."

The publishable key (the one starting with sb_publishable_, or the older "anon" key) is meant to sit in your website's JavaScript, so anyone can copy it. That means a function protected only by verify_jwt may be callable by a stranger who simply took the public key from your site, with no account at all. "Login required" has to be checked in your function too.

Trap 3: the admin client with no filter

The same docs add a warning: a handler that reads a shared table with the admin client (supabaseAdmin, which bypasses Row Level Security) "without filtering by the caller's ID returns every user's rows." A function that is login-protected but returns all rows is still a leak.

The mistakes we see

  1. verify_jwt = false on a function that is not a webhook.
  2. No login check in the code, because the AI assumed the platform did it.
  3. Webhooks that trust anything. A Stripe or GitHub webhook function should verify the provider's signature. Without it, anyone can post a fake "payment succeeded" message.
  4. The admin client used for everything, so Row Level Security never applies.
  5. Secret keys or the service key in the browser. See your API key is probably sitting in your JavaScript.

Step by step: check your functions

1. List your functions and their settings

In your project folder, open supabase/config.toml and look for lines like this:

[functions.send-invoice]
verify_jwt = false

Write down every function with verify_jwt = false. In the Supabase dashboard, the Edge Functions page shows the same setting for each function. If you have no config.toml, ask your AI tool: "Which of my Edge Functions have verify_jwt turned off, and why?"

2. Call each function as a stranger

You need your project address and your public (publishable or anon) key. Both are already visible in your app's JavaScript, so it is safe to use them for this test. Replace the capitals and run:

curl -i -X POST "https://YOUR-PROJECT.supabase.co/functions/v1/FUNCTION-NAME" \
  -H "apikey: YOUR_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Read the first line of the answer:

Only test your own project, and use an empty request like the one above. Do not send real data to a function you have not checked.

3. Read the function's first lines

Open each function's code. Near the top there should be something that checks who is calling, such as the withSupabase({ auth: 'user' }, ...) pattern from the docs, or code that reads the user from the token and returns a 401 if there is none. If the function goes straight to its work, it has no check.

4. Check what the function can touch

Search the code for service_role, supabaseAdmin and SUPABASE_SECRET. Any database query that uses these ignores your Row Level Security rules, so each one needs to be filtered by the logged-in user's ID.

5. Webhooks: look for the signature check

For Stripe, GitHub and similar, the function should verify the signature header (for Stripe, stripe-signature) before doing anything. The Supabase docs include a full Stripe example that returns a 400 when the signature is wrong.

How to fix it

  1. Turn verify_jwt back on for every function your own users call. In config.toml, remove the verify_jwt = false line, or set it to true, then redeploy.
  2. Check the caller in your code too. Use the Supabase pattern withSupabase({ auth: 'user' }, ...), which gives you a database client that follows each user's Row Level Security rules.
  3. Use the admin client only when you must, and always filter by the caller's ID from their token.
  4. Keep verify_jwt = false only where it belongs: webhooks (verify the signature in the code) and a few genuinely public functions such as a health check.
  5. Send keys in the right header. User tokens go in Authorization, API keys go in apikey, as the docs recommend.
  6. Rotate anything that may have leaked. If a function that used a paid service was open, check that service's usage page and consider a new key.

You can paste this into your AI tool: "List every Supabase Edge Function in this project. Turn verify_jwt on for any function called by logged-in users and add a check in the code that rejects requests without a valid user token. Use the RLS-scoped client, not the admin client, unless the function truly needs to bypass RLS, and filter by the caller's user ID. For webhooks, verify the provider's signature."

Related checks

Functions are one door into your app. Also check whether your database is open, whether your file storage is public and the pre-launch security checklist.

Check it for free

Greenlight Scan looks at your live app the way a stranger would and gives you a traffic-light verdict in about a minute. Run the free scan to find out whether anything is open. If you want the deeper check with step-by-step fixes, the $19 Full Scan goes through it all in detail.

Newsletter

The Friday leak report

One short email a week: the newest ways AI-built apps leak data, and the one-line fix for each. No spam, unsubscribe any time.