Lovable edge function public without auth: how to lock it down
To lock down a Lovable edge function that is public without auth, make sure the function requires a valid user token, then check the signed-in user inside the function code before it does any work. If the function used a secret key while it was public, rotate that key.
This guide shows how to test your functions from outside, how to fix each one, and how to read the logs for abuse. It also shows what to do for functions that must stay public, such as webhooks.
What does “edge function public without auth” mean?
It means that anyone who knows the function URL can call it, and the function does not check who the caller is. The caller does not need to sign in. The function runs with your server secrets, on your bill.
An edge function is server code that runs on the backend of your app. Lovable apps use edge functions for tasks that the browser must not do alone, such as calls to paid APIs, emails, and payments. The Lovable docs tell you to make API calls “in server-side code instead of in the browser” so that secrets stay off the client. That advice is correct. But a server function that anyone can call gives the same access to every visitor.
The URL is not secret. The address shows in the network tab of DevTools, so any visitor can copy it.
Two checks exist for a function on Supabase, and a function can fail at either one:
| Check | Where it runs | What it proves |
|---|---|---|
Platform JWT check (verify_jwt) |
Before your code runs | The request has a valid JWT |
| Your own user check | Inside your function | The request comes from a signed-in user that you accept |
The first check is not enough on its own. The Supabase function auth documentation states that the default is verify_jwt = true, so the platform validates the JWT before your handler runs. The anon key of your project is also a valid JWT, and it is in your frontend code. A caller who copies it can pass the platform check. For this reason you must also check the user in the function.
How does a function become public in a Lovable app?
There are three common ways.
- The JWT check is off. The function has
verify_jwt = falsein its configuration. A tool or a prompt can set this to clear a401error during development. The Supabase documentation warns that turning offverify_jwtto clear a 401 removes the platform check, and the function code becomes the only check. - The check is on, but the function does not read the user. The function accepts any valid JWT, including the anon key. It never asks who the user is.
- The function trusts a value from the request. The function reads a
user_idorrolefrom the request body and uses it. A caller can send any value.
Lovable does not state in its docs how it sets verify_jwt for each function. I did not verify what Lovable sets by default. Check each function in your project. Do not rely on a default.
How do I check whether my edge function is public?
Test each function from outside the app, with no login. This takes a few minutes.
- List your functions. Open the
supabase/functionsfolder in your repository, or the Edge Functions page of your Supabase dashboard. Write down each function name. - Find the URL. The form is
https://<project-ref>.supabase.co/functions/v1/<function-name>. The project ref is in your Supabase project URL. - Call it with no token. Use
curland send noAuthorizationheader:
curl -i -X POST "https://<project-ref>.supabase.co/functions/v1/<function-name>" \
-H "Content-Type: application/json" \
-d '{}'
- Read the status code. A
401means that the platform check is on. A200, or an error from your own code, means that the request reached your function. - Call it with the anon key only. Send the anon key as the bearer token. This is the key from your frontend:
curl -i -X POST "https://<project-ref>.supabase.co/functions/v1/<function-name>" \
-H "Authorization: Bearer <anon-key>" \
-H "apikey: <anon-key>" \
-H "Content-Type: application/json" \
-d '{}'
- Read the result again. If the function does its job for a caller who has only the anon key, the function does not check the user. The function is public in practice.
Warning: Do not test a function that sends emails, charges cards, or deletes data with real data. Use test data.
Also read the code of each function. Search for verify_jwt, getUser, getClaims, and Authorization. A function with none of these words does not check the user.
grep -rn "verify_jwt" supabase/config.toml
grep -rLE "getUser|getClaims|auth\.getSession" supabase/functions
The second command lists the function files that contain none of the three calls. Check each file in the list. Do not assume that it is unsafe or safe from the name alone.
The Lovable security docs say that its Deep scan flags “Functions and routes that anyone can trigger, upload to, or use to send messages without permission.” The Quick scan runs on each publish, but the Deep scan does not run by itself. Run it from the Security view. The docs also say that these tools “cannot guarantee complete security”, so use the curl test as well.
How do I fix it now? Steps in order of urgency
Do these steps in order. Steps 1 and 2 limit the loss. The next steps remove the cause.
Caution: Closing a function stops each caller that sends no valid token. Calls from signed-in users keep working if the app sends the user token. Test each page that calls the function.
- List what the function can do. Read the code. Write down each secret it reads, each table it writes, and each email or payment it triggers. Start with the functions that spend money or that read personal data.
- Take the most dangerous function offline if you must. Delete the function from the dashboard or remove the folder and deploy. A broken feature costs less than an open function that spends your API credit.
- Turn on the platform JWT check. Set
verify_jwt = truefor the function insupabase/config.toml, or remove the line, becausetrueis the default. The Supabase function configuration page shows the setting under the name of the function:
[functions.send-report]
verify_jwt = true
- Check the user inside the function. See the code in the next section.
- Remove any trust in the request body. Do not read
user_id,role, oris_adminfrom the body. Read them from the verified user. - Deploy the function again. Then run the two curl tests. The call with no token must return
401. The call with only the anon key must return401from your own check. - Rotate secrets that the function used or returned. See the section on damage.
- Add limits. Add a per-user rate limit and an input size limit. See API abuse protection.
If you use the --no-verify-jwt flag when you deploy or serve, remove it. The Supabase documentation warns that this flag “will allow anyone to invoke your Edge Function without a valid JWT.”
How do I check the user inside the function?
Create a Supabase client with the token from the request, and call getUser. Return 401 when there is no user. Run this check before any other work.
import { createClient } from "npm:@supabase/supabase-js@2";
const cors = {
"Access-Control-Allow-Origin": "https://your-app.example",
"Access-Control-Allow-Headers": "authorization, content-type, apikey",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: cors });
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401, headers: cors });
// From here, use user.id. Never read the user id from the request body.
const { data, error } = await supabase
.from("reports")
.select("id, title")
.limit(50);
if (error) return new Response("Error", { status: 500, headers: cors });
return new Response(JSON.stringify(data), {
headers: { ...cors, "Content-Type": "application/json" },
});
});
The anon key alone has no user, so getUser returns no user. The function returns 401. A signed-in user sends a user token, so the check passes.
This client uses the token of the caller, so the database applies Row Level Security (RLS) for that user. Do not create the client with the service_role key for a user request, because the service_role key skips RLS. If a function must use service_role, filter every query by user.id yourself. For the RLS side, read the Supabase RLS guide.
If the function needs an admin role, check the role from your own table or from a signed claim. Do not read it from the request body. A user must not be able to change the field that you check.
Set the allowed origin in cors to your real domain. A * value for a function that spends money lets any website call the function from a visitor’s browser. CORS does not replace auth, because a script outside a browser ignores CORS.
What if the function must be public?
Some functions must accept callers with no user token. The common case is a webhook from a payment or email provider. Set verify_jwt = false for these functions only, as the Supabase documentation shows for a payment webhook.
A public function needs its own proof of the caller. Use one of these:
| Function type | Proof to check |
|---|---|
| Payment or email webhook | The signature header, checked with the signing secret of the provider |
| Scheduled job | A secret value that only the scheduler knows, sent in a header |
| Public form | A rate limit per IP address, a size limit, and a bot check |
Check the proof before any other work. Reject the request with 401 if the proof is missing or wrong. Store the signing secret as a server secret, not in the code.
Keep each public function small. It must not return data or accept a table name from the caller.
How do I check the damage?
Read the logs for the period in which the function was public. Look for calls that your app did not make.
- Open the function logs. In your Supabase dashboard, open the Edge Functions page, select the function, and open its logs. I did not verify the current menu names, so look for the logs and invocation data of the function.
- Look for strange volume. Compare the number of calls with the number of your real users. A burst of calls from few sources is a sign of abuse.
- Look for callers with no user. If your function logs the user id, look for calls where it is missing.
- Check the bills of paid APIs. If the function called a paid API, read the usage page of that API for the period. Look for models, volumes, and days that you do not recognize.
- Check the outputs and the data. Read the list of sent emails and the rows that the function wrote. Look for recipients and rows that are not yours.
- Check what the function returned. If it returned secrets or personal data, assume that a caller read them.
Then rotate these secrets:
- Every API key that the function used. For an OpenAI key, see what to do about an exposed OpenAI key.
- The
service_rolekey, if the function used it. Rotate it in the Supabase dashboard, and update the server secrets. - Any signing secret or token that the function returned in a response.
Set the new values as secrets for the functions. The Supabase secrets documentation shows supabase secrets set NAME=value and supabase secrets list. Do not type a real key into a shared chat or a shared terminal history.
Note: The anon key is public by design, so you do not need to rotate it. Read the guide on the Supabase anon key to check that RLS protects each table.
How do I keep new functions safe?
Use these habits so that each new function starts closed.
- Ask for auth in the prompt. Tell Lovable that each edge function must check the signed-in user with
getUserand return401if there is none. - Review the config after each change. Search
supabase/config.tomlforverify_jwt = falsein each pull request. Each one needs a written reason. - Run the curl tests for each new function. Run the no-token test and the anon-key-only test before you publish.
- Use a separate key for each function that spends money. Set a spend limit at the vendor. One leak then has a small cost.
- Run the Lovable scans, and scan the live app. The Lovable scans read your project. A scan of the deployed app tests what a stranger can reach. For more on this platform, read the Lovable security guide.
When should I get help?
Get help in these cases:
- The logs show many calls from unknown sources, and the function used a
service_rolekey or returned personal data. - You see charges at a paid API that you did not cause, and you cannot trace them. Contact the support of that vendor with the dates and the key name.
- You do not know which functions exist, or which ones the app still calls.
- A public function writes to billing, roles, or user data, and you are not sure that it checks its caller.
- You have a legal duty to report a breach.
After the fix, run both curl tests on every function once more. Each function that is not a webhook must return 401 for a call with no token and for a call with only the anon key.
Common questions
Is an edge function with verify_jwt on always safe?
Which functions must stay public?
Do I need to rotate keys if the function was public?
Will turning on verify_jwt break my app?
Can the Lovable security scan find this problem?
Test your edge functions
A VibeEval scan finds the function URLs that your deployed app calls and reports the functions that answer a request that has no signed-in user.
14-day free trial · No credit card · Cancel anytime