Fix the Lovable CORS Error When an External API Is Blocked in Production
If your Lovable app throws "Access to fetch ... has been blocked by CORS policy" when calling an external API like OpenAI or Stripe in production, the browser is correctly refusing a cross-origin request the third-party server never approved. The real fix is not a header hack — it is to move the call server-side into a Supabase Edge Function, which also stops your API key from leaking to every visitor.
By Founder Name · Last verified: 2026-06-25
Why does my Lovable app get a CORS error calling an external API in production?
Because the browser enforces the same-origin policy. When your front end calls an API on a different domain, the browser first checks whether that server returns an Access-Control-Allow-Origin header naming your origin. Most third-party APIs (OpenAI, many payment and data providers) deliberately do not send that header for browser calls, so the browser blocks the response before your code ever sees it.
This is not a Lovable bug and it is not a bug in your code logic — the request often succeeds at the network level, but the browser discards the response because the server did not opt your origin in. That is why the error appears in the browser Console and Network tab, not in your application logic.
It frequently looks like it works in the editor preview and breaks only on your live domain. The Lovable preview runs on a lovableproject.com origin; your published site runs on your custom domain. An API that happened to allow one origin will block the other, producing the classic "worked in preview, broken in production" report.
What does the "Access to fetch has been blocked by CORS policy" error actually mean?
It means the browser made a cross-origin request and the response was missing the Access-Control-Allow-Origin header that would authorize your site to read it. The full string usually reads: "Access to fetch at 'https://api.openai.com/...' from origin 'https://yourapp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present."
Read the error literally — it names both the target URL and your origin. That tells you exactly which external call is being rejected. Two variants are worth knowing: a missing header (the server sent no CORS headers at all) and a preflight failure, where the browser's automatic OPTIONS request to the server was rejected before your real request ran.
Preflight failures show a second clue: "Response to preflight request doesn't pass access control check." Browsers send an OPTIONS preflight whenever a request uses a non-simple method or custom headers (for example an Authorization header). If the server does not answer that OPTIONS request with the right headers, the real request never fires.
| Console message | What it means | Correct fix |
|---|---|---|
| No 'Access-Control-Allow-Origin' header is present | Third-party API does not allow browser calls from your domain | Proxy the call through a Supabase Edge Function |
| Response to preflight request doesn't pass access control check | The OPTIONS preflight was rejected (often a custom Authorization header) | Handle OPTIONS in your edge function, return CORS headers |
| ...Allow-Origin: '*' but credentials mode is 'include' | Wildcard origin cannot be combined with cookies/credentials | Set an explicit origin, not '*', when sending credentials |
| CORS error only on live domain, not in preview | API allow-listed the lovableproject.com preview origin but not production | Move the call server-side so origin no longer matters |
| Failed to fetch (no CORS line) | Network/DNS failure or the API is down — not actually CORS | Check the Network tab status code before assuming CORS |
Related: debugging Lovable production issues · the Integrations & APIs hub
Why is calling OpenAI or any keyed API directly from the browser dangerous?
Because a browser request ships your API key to the user's machine. Anyone can open DevTools, read the key from the Network tab or bundle, and run requests on your account — driving up your bill or abusing your provider quota. The CORS block you are fighting is often the browser quietly protecting you from a far worse problem: a publicly exposed secret.
This is the most common security misconfiguration we find in vibe-coded apps: a secret that belongs only on a server has been wired into client code so the demo would work. The fix and the CORS fix are the same move — run the call on a server you control, where the key stays in an environment variable the browser never sees.
Treat any key already shipped to the browser as compromised. Rotate it at the provider, remove it from all front-end code, and re-add it only as a server-side secret. Do not skip the rotation step: a key that was live in production for even a short time should be assumed to have been scraped.
Related: move secrets to edge functions · Lovable env secrets best practices
How do I fix the CORS error by proxying through a Supabase Edge Function?
Create a Supabase Edge Function that receives the request from your front end, calls the external API server-side using a secret key, and returns the result with CORS headers your app accepts. Server-to-server calls are not subject to the browser same-origin policy, so the third-party API responds normally and your key never reaches the client.
A minimal Deno edge function looks like this. The shared corsHeaders block is what makes the browser accept the response, and the OPTIONS short-circuit is what satisfies the preflight: "const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS' };"
Inside the handler: "if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });" then call the API: "const r = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { Authorization: 'Bearer ' + Deno.env.get('OPENAI_API_KEY'), 'Content-Type': 'application/json' }, body: await req.text() });" and finally "return new Response(await r.text(), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } });"
From the front end, point your fetch at the function instead of the provider: "await fetch('https://YOUR-PROJECT.supabase.co/functions/v1/ai-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + SUPABASE_ANON_KEY }, body: JSON.stringify(payload) })". The anon key here is safe to ship — it is meant for the browser; the provider key stays server-side.
- In your Supabase project, create a new Edge Function (for example, ai-proxy) under supabase/functions.
- Store the provider key as a secret: run "supabase secrets set OPENAI_API_KEY=sk-..." so it lives only on the server, never in VITE_ variables.
- In the function, handle the OPTIONS preflight first, then forward the real request to the external API with the secret key in the Authorization header.
- Return the API response to the browser with Access-Control-Allow-Origin set to your domain (or '*' for a public, non-credentialed endpoint).
- In your Lovable front end, replace the direct fetch to the external API with a call to your edge function URL.
- Deploy the function, then test from your live domain (not just the preview) and confirm the Console shows no CORS error.
Why does the CORS error appear in production but not in the Lovable preview?
Because the preview and your live site run on different origins. The Lovable editor preview is served from a lovableproject.com subdomain; your published app runs on your custom domain. If the external API ever allow-listed the preview origin, or if a browser extension relaxed the rule locally, the call appears to work in preview and then fails the moment real users hit production.
This is a specific case of a broader pattern we call the production gap: behaviour that only diverges once the app leaves the editor sandbox. Origin-dependent CORS is one of the most common. Proxying the call server-side removes the variable entirely — origin no longer matters because the browser is talking to your own Supabase domain, not the third party.
A second production-only trigger is a vanished environment variable: the key worked in the editor but was never added to the deployed environment, so the server call fails with a 401 that surfaces as a broken response in the browser. Always confirm your secret is set in the deployed Supabase project, not only in the local editor.
How do I verify the CORS fix actually works before calling it done?
Test from your live domain, not the editor preview, and watch the browser Network tab. A correct fix shows the request going to your Supabase function URL (not the third-party API), a 200 status, and zero red CORS lines in the Console. Then confirm the provider key never appears anywhere in the front-end bundle or network requests.
- Open your published site (custom domain), then DevTools (F12) and go to the Network tab.
- Trigger the feature that calls the external API and find the request — it should point at your .supabase.co/functions/v1/ URL.
- Confirm the response status is 200 and the Console shows no "blocked by CORS policy" line.
- Search the Network tab and bundle for your provider key (for example, sk-) — it must not appear in any browser request or source file.
- Test the OPTIONS preflight: if the request uses a custom header, confirm the preflight returns 200 with the CORS headers.
- Repeat once from an incognito window and a mobile browser to rule out a cached old build serving the pre-fix code.
When should I stop fighting CORS myself and get an engineer?
If you have moved the call to an edge function and still see CORS errors, or you are unsure whether your API key was exposed before you fixed it, bring in a senior engineer. Misconfigured proxies can leak secrets through logs or over-permissive headers, and a key that was public even briefly needs rotation. A specialist closes the hole, verifies nothing leaked, and ships a clean build.
Signs it is time to escalate: the preflight keeps failing despite an OPTIONS handler; the function returns 401 or 500 you cannot trace; or you have multiple keyed integrations (OpenAI plus Stripe plus a data API) all calling from the browser. We restore a working build, move every secret server-side, and leave you a written summary of what was exposed and what we rotated.
For a live app actively leaking a key in production, treat it as urgent — every hour the key is public is billable abuse risk on your account. We offer an emergency review and can usually rotate, proxy, and redeploy the same day.
Related: the Lovable App Rescue service · book a free audit call
Frequently asked questions
Can I just add an Access-Control-Allow-Origin header to fix the CORS error?
Will a CORS browser extension or proxy fix this permanently?
Why does the API call work in the Lovable preview but fail on my live domain?
Is it safe to put my OpenAI API key in a VITE_ environment variable?
What is a Supabase Edge Function and why does it fix CORS?
My fix passes preflight locally but still fails in production — why?
Do I need to handle the OPTIONS request in my edge function?
How do I know if my API key was already exposed before I fixed this?
Can I use Access-Control-Allow-Origin: '*' or do I need my exact domain?
How fast can you fix a CORS error that is leaking my API key in production?
App down or leaking data? Get an expert on it within 24–48h.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.