Your Supabase Edge Function works locally but fails in production: why
Quick answer: Why does my Supabase Edge Function work locally but fail after deploy?
Locally, Edge Functions read secrets from supabase/functions/.env, but that file isn’t deployed. In production, every secret, like a Stripe or OpenAI key, has to be set separately in the dashboard or with supabase secrets set. Until then the function reads undefined and fails. SUPABASE_URL and the project keys are provided automatically, so don’t set those.
On your machine the function answers in milliseconds. After deploying, it returns 500, logs something about undefined, or just quietly stops doing its job. Payment confirmations don’t arrive, welcome emails aren’t sent, the AI feature spins forever. Nine times out of ten, the code is fine and the production function is missing something your laptop had.
What it looks like
- The function returns
500 Internal Server Errorin production only - Logs show an API key, URL or token as
undefined, or an error like “invalid API key” from Stripe, OpenAI or Resend - A browser call fails with a CORS error, or a
401 - A background job (payment, email, AI call) just never happens, with no error the user sees
Cause 1: secrets that only exist on your machine
Locally: supabase/functions/.env
Per Supabase’s environment variables docs, local Edge Functions read secrets from supabase/functions/.env, loaded when you run the local stack. That file stays on your computer (and should be in .gitignore). Deploying the function doesn’t upload it.
Production: set them separately
Production secrets are set in the Supabase dashboard under Edge Function Secrets, or with one CLI command that pushes every value from an env file:
supabase secrets set --env-file .env
A few details from the docs that trip people up:
- Functions read a new secret immediately. You don’t need to redeploy.
- Creating or deleting a production secret needs the Owner or Administrator role. Developers can only view them.
- Secret names can’t start with
SUPABASE_, because that prefix is reserved. - The name must match exactly what the code reads:
STRIPE_SECRET_KEYisn’tSTRIPE_KEY.
On Lovable Cloud? Lovable’s secrets docs say secrets are injected into your Edge Functions automatically and usually requested through a secure form in the chat. You can check and add them under More → Cloud → Secrets. If you once clicked Skip on a secret prompt, anything that depends on it won’t work until you add it.
Secrets you don’t need to set
Supabase injects these for you: SUPABASE_URL, SUPABASE_DB_URL, SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS and SUPABASE_JWKS, plus the legacy SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY. If your code reads a different name for the project URL or keys, for example a VITE_-prefixed one copied from the frontend, it gets undefined in production.
Never put the .env in Git
A committed .env exposes every secret in it to anyone who can read the repository, and it stays in the Git history even after you delete it. If that happened, rotate the keys. Our leaked key guide explains the order to do it in.
How to confirm it’s this
Don’t guess from the browser. Open the function’s logs: in the Supabase dashboard go to Functions, pick the function, and look at Invocations (status codes, request and response) and Logs (uncaught exceptions and your own console.log output), as described in the logging docs. On Lovable Cloud, logs are under More → Cloud → Logs. A missing secret usually shows up as an error that names the variable, or as the third-party API rejecting an empty key.
Not sure which of these is your problem? The free health check looks at your live app in seconds, or request a free scan and we’ll tell you what we find.
Other suspects
CORS: the browser blocks the call
If the function is called from your frontend, the browser first sends a “preflight” OPTIONS request. Supabase’s CORS guide says you need to handle it: answer the preflight and include CORS headers on every response, including error responses. A function that crashes before sending headers often shows up as a CORS error, which hides the real problem. Check the logs first.
JWT verification: calls without a login are rejected
By default, Edge Functions require a valid JWT in the Authorization header, according to the function configuration docs. Calls from the Supabase client in your app include it automatically. A webhook from Stripe or another service doesn’t, so it gets a 401. Supabase lets you turn verification off per function (verify_jwt = false, the documented example is a Stripe webhook). Do that only for functions that check the caller another way, such as a webhook signature, because otherwise anyone can call them.
The database says no
The function runs, but its query fails with 42501 or returns nothing. That’s a grants or RLS issue, not a function issue: see the October 30 checklist and the RLS check.
The latest version was never deployed
You changed the function locally but production still runs the old code. Check the deployment time in the dashboard against your last change.
When it’s a payment or webhook function
Webhook functions combine all of the above: secrets (the signing secret), JWT verification and database access. If customers pay and nothing unlocks, follow Stripe paid, feature not unlocked, which shows how to read Stripe’s delivery log.
Get it checked
Several functions, several keys, not sure which is broken?
The $49 Audit is a security & stability review of your app, including its Edge Functions, secrets and callers, with a full report in 24 hours and copy-paste fix prompts. The $199 Fix repairs up to 5 issues as a pull request with tests within 72 hours. Not fixed? Full refund. Guard ($79/mo) reviews every push to catch leaks and regressions early.
FAQ
Do I need to redeploy after setting a secret?
No. According to Supabase’s docs, deployed functions read a new secret immediately.
Should I set SUPABASE_URL myself?
No. SUPABASE_URL, SUPABASE_DB_URL and the project keys are injected automatically, and names starting with SUPABASE_ are reserved.
Is it safe to put my Stripe key in a secret?
Yes. An Edge Function secret is the right place for it. Never put it in a VITE_ or NEXT_PUBLIC_ variable, because those end up in the JavaScript every visitor downloads.
Where do I see the real error?
In the function’s Invocations and Logs in the Supabase dashboard (Functions → your function), or More → Cloud → Logs on Lovable Cloud.
Why do I get a CORS error instead?
The browser’s preflight request isn’t answered with CORS headers, or the function crashed before sending them. Check the logs to see which.
Related guides
- Stripe says “paid”, your app says “free”: why the feature didn’t unlock
- Leaked your Supabase service_role key in a vibe-coded app? What to do now
- Supabase’s October 30 change is live: a 5-minute checklist for new tables
- The launch checklist for apps built with Lovable, Bolt, Cursor, Replit and v0
- Your app calls localhost or hits a CORS error after deploy: here’s why
Launching soon? Run through our launch checklist for AI-built apps: 14 checks, each linked to the full guide.