TL;DR: Every Supabase edge function is a public URL. The verify_jwt flag is half an auth check: it proves a caller is logged in, not that the caller may act. Real edge function auth has three layers. Check what the user may do inside the function. Keep the service_role key server-side only. Lock CORS to your own domain. AI editors like Lovable, Bolt, Cursor, and v0 skip all three by default. Here is the checklist, and a free scan that tests your live app in about 30 seconds.

An edge function feels private. It is not. It lives at a fixed public address, and anyone on the internet can call it. Your frontend is just one caller among many. AI editors scaffold these functions as if only your app will ever knock. That one wrong idea drives every gap on this page.

QuestionAnswer
Does verify_jwt handle auth for me?
No. It checks the token, not the permission. You still need a check inside the function.
Where does the service role key live?
Server-side secrets only. Never in client code. Never in the repo.
Is wildcard CORS a real risk?
Yes. It removes a browser defense, and AI scaffolds ship it by default.
Does RLS make function auth optional?
No. Service role calls skip RLS. You need both layers to hold.
How do I check my app?
Scan your live URL. No code access. About 30 seconds.
What does it cost?
The scan is $0. The full audit report is $499, one time.

An edge function is a public URL. Treat it like one.

Every function you deploy gets an address like https://your-project.supabase.co/functions/v1/send-invite. There is no hidden network in front of it. If your frontend can reach it, so can a stranger with curl. So can a script that found the URL in your page source.

AI editors write these functions to serve one caller: your own app. They name them "internal" in comments. The name does nothing. The URL is public, and the code inside is the only gate. The whole question of Supabase edge functions auth comes down to what that code checks before it acts.

The verify_jwt gap: logged in is not allowed

Supabase gives each function a verify_jwt setting. When it is on, a request must carry a valid token in the Authorization header. AI scaffolds lean on that flag and stop. It looks like auth. It is half of auth.

The flag proves one thing: the caller is a logged-in user. Any logged-in user. It does not prove the caller owns the row they are about to change. It does not prove they hold the admin role the action needs. We have reviewed functions where any fresh signup could fire an admin-only action. The token was valid. The check on what that token could do never existed.

A real check does three things inside the function:

Where the token lives on the client matters too. Many AI builds park it in localStorage, where any injected script can read it. We cover that trade-off in storing JWTs in localStorage.

The service_role key stays on the server. No exceptions.

The service_role key bypasses Row Level Security. All of it. Every table, every row. It exists so a trusted server process can do admin work. That power is the whole point — and the whole risk.

AI editors copy this key to wherever the code stops erroring. Sometimes that is frontend code, which ships it to every visitor. Sometimes it is hardcoded in the function file, which puts it in your repo. And often it lands in a function that has no auth check in front of it at all.

What this means in practice

A public function that holds the service_role key with no auth gate is an open database. Anyone with the URL has admin access. One curl command pulls every row of every table.

The fix is short. Store the key with supabase secrets set, read it from the environment inside the function, and put a permission check ahead of every use. If you are not sure which key does what, read anon key vs service role key before you ship.

CORS: the quiet hole in the scaffold

CORS headers tell browsers which sites may call your function. The AI-generated template sets Access-Control-Allow-Origin: * and moves on. The app works, the demo passes, and nobody looks at that line again.

The wildcard does not break anything. That is the trap. It strips a defense the browser gives you for free. With it gone, a hostile page can call your function from someone else's tab. Pair it with a weak JWT check and you get a two-part failure: any origin may call the function, and the function trusts anyone with a token. Two half-checks add up to zero.

Lock the header to your real domain. Handle the OPTIONS preflight and send the same strict headers on error replies too. Many scaffolds only set them on the happy path. Our guide to CORS misconfiguration in vibe-coded apps walks through the exact header set.

The five scaffold mistakes we keep finding

  1. No permission check on "internal" functions. The comment says internal. The URL is public.
  2. The service_role key hardcoded in the function file. It belongs in a server-side secret.
  3. Wildcard CORS left from the template. Works in the demo. Open in production.
  4. No rate limits on anything. A single script can hammer an auth endpoint all night. Our k6 engine reports this word for word as "No rate limiting detected under concurrent load." (k6Scanner.js:170). Test one endpoint yourself with the free API rate limit checker.
  5. RLS policies that grant everything. A policy of USING (true) turns RLS on in name only. Everyone still gets in.

RLS is your second layer. Check that it is real.

Row Level Security is meant to hold even when a function check slips. But it only helps if it is set up right. Two questions decide it: is RLS on for every table, and are the policies owner-scoped? A lot of vibe-coded builds fail at least one. CVE-2025-48757 showed what that looks like at scale. 170 or more AI-built apps were left readable. RLS was off or too loose. Same stack, same default, same failure.

The two layers interact. A function that runs with the service_role key skips RLS by design. So a weak function check plus strong RLS still leaks. And a strong function check plus USING (true) leaks the other way. You need both layers to hold at once. Our RLS policies guide covers writing owner-scoped rules, and the free RLS checker probes your live app for the gap.

The three layers, side by side

LayerWhat it actually doesThe AI defaultThe fix
verify_jwtConfirms the token is valid and not expiredOn, and treated as full authKeep it on, then check role or ownership inside the function
service_role keyBypasses RLS for trusted server workHardcoded, or used with no gate in frontServer-side secret plus a permission check before every use
CORSTells browsers which origins may callAccess-Control-Allow-Origin: *Lock to your domain; cover preflight and error paths
RLSRow-level gate at the databaseOff, or USING (true)On for every table, owner-scoped policies

Why this keeps happening

The tools are not careless. They aim at the wrong target. Lovable, Bolt, Cursor, and v0 chase a feature that runs, not one that resists abuse. The outside numbers say how often that shows: Symbiotic Security scanned 1,072 AI-built apps and found 98% had at least one flaw, with 16% carrying critical issues. CMU's SusVibes study put at least one security flaw in 61% of AI-written code. Across 100 apps we scanned ourselves, the average Launch Readiness Score was 42/100.

None of that is a reason to drop the tools. It is a reason to run a second pass on the auth layer. That is the one place where "it works" and "it is safe" look the same from the outside.

One legal note before launch

If your edge functions decide who can read personal data, a weak check is not just a bug. Under GDPR it is a data leak waiting to happen. And if your app has an AI-facing feature and EU users, more rules kick in. Article 50 of the EU AI Act applies from August 2, 2026. Its transparency rules carry fines up to €15 million or 3% of global turnover. Our GDPR guide for vibe-coded apps covers the data side, and the Compliance Wing checks both.

The edge function auth checklist

  1. List every deployed function. Each one is a public URL.
  2. Keep verify_jwt on. Then add a role or ownership check inside each function.
  3. Move the service_role key to supabase secrets set. Grep your repo and bundle for it.
  4. Replace wildcard CORS with your real domain. Cover preflight and error responses.
  5. Rate-limit auth, reset, and invite endpoints.
  6. Turn on RLS for every table. Owner-scoped policies. No USING (true).
  7. Re-test from outside after every fix. Fixes interact.

What checking this costs

The free scan is $0 and takes about 30 seconds. You give it your live URL. You get a Launch Readiness Score out of 100 across security, reliability, performance, and monitoring. The full Launch Readiness Audit is $499 one time — a branded report with every finding, a ranked fix plan with time estimates, and a benchmark against 200+ audited apps. Want the fixes done for you? Done-for-you setup starts at $1,999 with a named CTO. The full picture of what we probe on this stack is on the Supabase security audit page.

FAQ

Does Supabase verify JWTs for me?

Only in part. The verify_jwt flag confirms a request carries a valid token. It does not confirm the user may touch the row or resource at hand. You still need a permission check inside the function.

Is it safe to use the service role key in an edge function?

Only if the key lives in a server-side secret and the function checks who is calling first. The service role key bypasses Row Level Security. A public function that holds it with no auth gate is an open database.

What is the most common CORS mistake in edge functions?

Leaving the wildcard Access-Control-Allow-Origin: * from the AI scaffold. The app still works, so nobody looks twice. But it removes a browser defense that matters once your JWT check is weak.

Do I still need auth checks if RLS is on?

Yes. RLS is the second layer, not a replacement. A function that runs with the service role key skips RLS entirely. And a policy of USING (true) grants access to everyone anyway. Check both layers.

How do I test my edge functions without code access?

Call them the way a stranger would. Every edge function is a public URL. Our free scan tests your live app from the outside, the same way an attacker starts. No install, no repo access, about 30 seconds.

What does a Supabase edge functions audit cost?

The scan is $0 and gives you a Launch Readiness Score out of 100. The full Launch Readiness Audit is $499 one time: a branded report, a ranked fix plan with time estimates, and a benchmark against 200+ audited apps.

The pattern here matches every stack in our vibe coding security guide: the code works, the defaults do not. Your edge functions run. Whether they check who is calling is a separate question — and it takes 30 seconds to start answering it.

Is your auth layer actually finished?

Scan your live app. No code access. A Launch Readiness Score out of 100, in about 30 seconds.

Run the free scan — $0