Security basics
Hide an API key: move it to a Supabase Edge Function
Hiding an API key means moving it off the browser, and a Supabase Edge Function is the smallest place to put it. Two steps around the move matter more.

In short
- Nothing in a browser can keep a secret, so hiding an API key means moving it somewhere that can. A Supabase Edge Function is the smallest server you can get.
- The move is four steps. Rotate the key you already published before you start, because the copy in your old bundle stays readable after you take it out.
- A deployed function demands a token by default, and the public key in your own frontend satisfies that demand. Checking who is calling is a separate step, and it is the one that stops a stranger spending your quota.
Every guide about a leaked API key ends in the same place: move it to a server. Then it stops. You are left holding an app you built in Lovable, Bolt or Cursor, no server anywhere in sight, and a sentence that assumes you already know what to do next.
Here is the part guide after guide leaves out. Moving the key into a Supabase Edge Function takes it out of your page, and by itself it does not stop a stranger using it. Supabase's own documentation says why: the check a deployed function runs by default also accepts your publishable key, and your publishable key is in your frontend for anyone to copy. Hiding the key is the straightforward half. The half nobody writes about is deciding who your new function answers to.
Think of the key as the master key to a stockroom. Right now it is taped to the inside of the shop window, where anyone who stops to look can read it. An Edge Function is a back room with a hatch: the key goes in there, and visitors ask through the hatch instead of walking in and helping themselves. Whether that is an improvement depends on who the hatch opens for.
Where should I put my API key instead of the frontend?
Anywhere that is not the browser. An Edge Function is the smallest version of that you can get.
A browser cannot keep a secret. Everything your page needs in order to run gets
downloaded by every visitor, and a visitor can read all of it: the code, the
images, the values compiled into the code. That is not a fault in your builder.
It is what a web page is.
Which API keys are safe in your frontend
is the long version; the short one is that an sk_ key, an OpenAI key or a
Supabase secret key was never going to survive being shipped to a browser.
A Supabase Edge Function is a small piece of code that runs on Supabase's machines. It can read secrets you set on your project, it answers at a web address of its own, and your app calls it by name. You write one file and Supabase runs it, so there is no server for you to rent, patch or keep awake.
Rotate the key you already published, before you move anything
The key sitting in your frontend today is already public, and it stays public after you take it out of the code.
Every visitor who loaded your site downloaded it. So did every crawler, and there are automated ones walking the whole web looking for exactly these strings. Your old bundle is also still in your version history and in whatever caches have a copy of your page. Removing a line from a file you control does nothing about a value that has already been handed out.
So the order is: create a new key at the provider, hold on to it for a moment, and revoke the old one once the function below is live. Then read your billing page and your usage logs for the whole period the old key was out there. Rotating stops what happens next; it has no effect on what already happened. If the key was a Supabase secret key, the order to work in has a wrinkle worth reading first.
The four steps
Store the secret, write the function, deploy it, then change your app to call the function instead of the vendor.
| Step | On the command line | In the dashboard |
|---|---|---|
| 1. Store the secret | supabase secrets set MY_API_KEY=… | Edge Functions → Secrets, then Key and Value |
| 2. Write the function | supabase functions new forward-request | Edge Functions → Deploy a new function → Via Editor |
| 3. Deploy it | supabase functions deploy | The Deploy button under the editor |
| 4. Call it from your app | supabase.functions.invoke('…') | Same, in your app's code |
Inside the function the secret arrives as an environment variable, which is a named value the code can read and nobody outside can. Supabase's docs for Edge Function secrets have the full reference, and three details in it save a confusing hour:
- A secret name cannot start with
SUPABASE_. That prefix is reserved for the values Supabase sets for you, and both the dashboard and the API refuse it. - A new secret is readable immediately. You do not redeploy the function after changing one.
- Local and production are separate. The local stack reads
supabase/functions/.env, which is a different place from the secrets on your live project, so set the value in both or the function works on your machine and fails once deployed.
Then delete the old variable from your frontend. If it was named VITE_,
NEXT_PUBLIC_ or EXPO_PUBLIC_, that prefix was an instruction to compile the
value into the bundle, and the prefix is the whole
story of how it got there.
Does a token requirement mean only my users can call it?
No, and this is the one thing in this article worth reading twice.
Supabase turns a check called verify_jwt on by default. It inspects the
Authorization header before your code runs and refuses the request if nothing
valid is there. That sounds like a door only your users can open. It is not,
because Supabase also documents that the same check
accepts a publishable or secret key
on either header, for compatibility with older projects, and adds plainly that
the check alone does not authenticate a caller who sends only an API key.
Your publishable key is in your frontend. That is correct and it is meant to be there. It also means anyone who opens your site, reads the key and sends it to your new function gets past the platform check and into your code.
In the stockroom, verify_jwt is a doorman who checks that you are holding a
visitor pass. Every visitor has one, because you hand them out at the door. The
receptionist at the hatch is a different job, and it is the one that asks which
visitor you are.
Locking the function down, so you have not built an open proxy
Decide which callers your function accepts, and write that decision into the function.
Supabase ships a wrapper for this, so it is a line rather than a project. Set
auth: 'user' and the function accepts a signed-in user's token and hands your
code a database client already scoped to that user's Row Level Security rules:
import { withSupabase } from 'npm:@supabase/server@1'
export default {
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
// ctx.userClaims is who is calling. Reject anything you do not want here,
// then call the vendor with the secret from the environment.
return Response.json({ ok: true })
}),
}
The Securing Edge Functions
page lists the other modes, and two of them matter for ordinary apps. A function
called by another machine rather than by a browser uses auth: 'secret', with
the secret key sent on the apikey header. A function that receives webhooks
from Stripe or GitHub cannot use either, because those providers have no token
of yours; it sets verify_jwt = false and checks the provider's signature
inside the handler instead.
Whichever you pick, decide deliberately what happens when a caller you did not expect arrives. Supabase's example of a function that may accept every caller is a health check, and a health check costs nothing when a stranger calls it. A function that forwards a paid API call bills you for each one.
Can I use service_role in an Edge Function?
Yes. A function is the one place a Supabase secret key belongs.
You do not paste it in, either. Supabase puts the project's keys into the
function's environment for you, so the code reads them from there rather than
from a secret you set. Newer projects get SUPABASE_SECRET_KEYS and
SUPABASE_PUBLISHABLE_KEYS, each a small dictionary of named keys; older
projects get SUPABASE_SERVICE_ROLE_KEY and SUPABASE_ANON_KEY under their
original names. What changed when Supabase renamed its
keys covers which pair you have.
Use the secret key for work that genuinely has to see every row, such as writing an audit record the user must not be able to edit. For anything a user is reading about themselves, use their own token and let your Row Level Security rules do the filtering, which is what they are for.
Verifying it from the browser, which is the only proof
Load your live site, open the network tab, and look at what your browser actually sends.
The two things to check:
- No request carries the key. Click through the part of your app that used
to call the vendor. The outgoing request should go to
…supabase.co/functions/v1/your-function, and the only credential anywhere in it should be your publishable key or your user's own token. - The key is not in the downloaded code. Use your browser's search across all loaded files and paste in the first dozen characters of the old key. No result is the answer you want.
A rebuild and a redeploy is what removes a value from the bundle, so a key that still turns up in the search usually means the frontend went out before the variable came out of it.
Our free scan does the second check from outside and names what it can read in your live bundle, including which Supabase key you shipped. It takes about 20 seconds and needs no account: scan your app.
Doing this from inside Lovable, Bolt or Replit
Use the Supabase dashboard. There is no terminal step in it anywhere.
Open your project, choose Edge Functions in the sidebar, then Deploy a new function → Via Editor. Supabase's dashboard quickstart has the walkthrough with screenshots, and the templates it offers include one for proxying an AI provider, which is the exact shape of this job. Deployment takes somewhere between ten and thirty seconds, and the function is then live at an address of its own. Your secret goes on the Edge Function Secrets page in the same section.
One caveat that is worth knowing before you rely on it: Supabase says the dashboard editor has no version control, no versioning and no rollbacks, and recommends it for quick work rather than for code you intend to keep. Pressing Deploy overwrites what was there. If the function ends up doing something you care about, download it from that page and keep the file somewhere.
There is also a Replit-shaped version of this question, because Replit has its own secrets store and your app runs its own server there. What Replit Secrets actually do is that version.
When an Edge Function is the wrong answer
Two cases, and in both of them moving the key costs you work and buys nothing.
The key was publishable. A Stripe pk_live_ key, a Supabase publishable or
anon key, a Mapbox public token: these are designed to sit in a browser, and
the protection is somewhere else. Putting one behind a function adds a hop and
removes no risk. Which keys those
are is the list.
The key can be restricted to your own site. Google's browser keys are the common example: you can limit one to your domains in the Google console, which is the fix Google supports for exactly this situation. The key is still readable and it stops being useful to anyone who copies it.
Everything else belongs on a server. There is no publishable variant of an OpenAI or Anthropic key, which is why a model provider key in your frontend has no setting that saves it, and no amount of minifying hides a Stripe secret key from somebody reading your page.
What to do right now
What to do
- Rotate the key first. Create a new one at the provider, put it in the function's secret, and revoke the old one once the function is live.
- Store the secret on your Supabase project, not in your repository and not in a
VITE_variable. Set it for local development and for production separately. - Deploy a function that uses the secret, and change your app to call the function by name instead of calling the vendor.
- Add a check on who is calling.
verify_jwton its own accepts the public key from your own frontend, so it does not keep a stranger out. - Delete the old variable, rebuild, and confirm from your browser's network tab that nothing going out carries the key.
- Read your billing and usage for the whole period the old key was public. Rotation stops the next charge and not the last one.
If you would rather work through your whole app rather than one key, the 10-minute security checklist covers this alongside the other things worth closing in a newly launched app.
FAQ
Where should I put my API key instead of the frontend?
Anywhere that is not the browser. A Supabase Edge Function is the smallest option: you store the key as a secret on your project, write one small file that uses it, and your app calls that function by name instead of calling the vendor directly. Other answers that work the same way are a serverless route on your host, or any server of your own. What they have in common is that the key is read where your visitor cannot see it.
What is a Supabase Edge Function?
A small piece of code that runs on Supabase machines rather than in your visitor browser. It answers at a web address of its own, it can read secrets you set on your project, and your app calls it with supabase.functions.invoke. You write one file and Supabase runs it, so there is no server for you to rent or maintain.
Do I need to rotate my key after moving it?
Yes, and do it first. The key that was in your frontend has been downloaded by every visitor and by every crawler that read your site, and taking it out of the code does nothing about the copies. Create a new key at the provider, put the new one in your Edge Function secret, and revoke the old one. Then check billing and usage for the period the old key was live.
Can anyone call my Edge Function?
By default a function refuses a request with no token at all, but that check is weaker than it sounds. Supabase documents that the platform check also accepts your publishable or secret key, and your publishable key is in your frontend where anyone can read it. So the check stops an empty request and not a determined one. To accept only your signed-in users, verify the caller inside the function.
Can I use service_role in an Edge Function?
Yes. An Edge Function is the one place a Supabase secret key legitimately belongs, and Supabase puts the project keys into the function environment for you, so you never paste one in. Use it for work that has to see every row, and use the caller own token for anything that should obey your Row Level Security rules.
Can I do this without a terminal?
Yes. The Supabase dashboard has an Edge Functions section where you can write a function in the browser, press Deploy, and set your secret on the Edge Function Secrets page. Supabase notes that the dashboard editor keeps no version history, so it suggests the editor for quick work and the command line for anything you intend to keep.