OpenAI or Stripe keys in your AI-built app? Move them to a server function
Anyone can copy a key from your frontend. Which keys are safe in the browser, how to find the ones that are not, and how to move them to a Supabase Edge Function.
4 min read
You asked your AI tool to add a chatbot, payments or email to your app. It worked on the first try, and the reason it worked so fast is often that the API key went straight into the frontend code.
Everything in the frontend is sent to every visitor’s browser. If a key is there, anyone can open the developer tools, copy it and use it, on your account and your bill. Automated scanners look for exactly these keys on public websites.
Which keys can be in the browser
Not every key is a secret. Many services issue two kinds on purpose:
| Safe in the browser | Never in the browser |
|---|---|
Stripe publishable key (pk_live_…) |
Stripe secret or restricted key (sk_live_…, rk_live_…) |
| Supabase publishable or anon key | Supabase secret or service role key (what to do if it leaked) |
| Analytics and tag IDs | OpenAI, Anthropic and other AI keys (sk-…) |
| Map keys restricted to your domain | Email sending keys (Resend re_…, SendGrid, Postmark) |
The rule: if the key lets someone spend money, read private data or send something in your name, it belongs on a server.
What a leaked key costs depends on the service. An AI key can produce a large bill in hours. A Stripe secret key can read your customers and issue refunds. An email key lets someone send phishing from your domain, which also damages your delivery for months.
Find keys in your live app
Open your app, press F12, go to Sources (Chrome) or Debugger (Firefox), and search all files (Ctrl+Shift+F or Cmd+Option+F) for: sk-, sk_live, rk_live, re_, sb_secret_, service_role.
If you have the code locally, build it and search the output, which is exactly what visitors receive:
npm run build
grep -rEn "sk-[A-Za-z0-9_-]{20,}|sk_live_|rk_live_|re_[A-Za-z0-9_]{20,}|sb_secret_|service_role" dist/A common source is the environment file. In Vite projects, which includes most apps from Lovable and Bolt, every variable starting with VITE_ is copied into the browser code. In Next.js the same applies to NEXT_PUBLIC_. A secret in VITE_OPENAI_API_KEY is public, no matter that it lives in .env.
Fixing it
1. Replace the key first
Moving the code doesn’t un-leak the key. In the provider’s dashboard, create a new key and delete the old one. Do this first, even if you think nobody has seen it.
While you are there, set a monthly spending limit on AI providers. It turns a potential disaster into a capped one.
2. Store the new key as a server secret
With Supabase, Edge Functions read secrets from their environment. Add the key in the dashboard under Edge Functions → Secrets, or with the CLI:
supabase secrets set OPENAI_API_KEY=sk-your-new-keyLovable has its own place for secrets and asks for them when an integration needs one, instead of putting them in code. On Vercel or Netlify, use environment variables without the VITE_ or NEXT_PUBLIC_ prefix, read only by server functions.
3. Call the service from a server function
The browser calls your function; your function calls the service with the secret key. A minimal Edge Function for an AI feature:
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req) => {
// 1. Only signed-in users may call this function.
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')! // public key: it only identifies the project
);
const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '');
const { data: { user } } = await supabase.auth.getUser(token);
if (!user) return new Response('Unauthorized', { status: 401 });
// 2. Accept only what the feature needs.
const { question } = await req.json();
if (typeof question !== 'string' || question.length > 2000) {
return new Response('Bad request', { status: 400 });
}
// 3. The secret key never leaves the server.
const response = 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: JSON.stringify({
model: Deno.env.get('OPENAI_MODEL'),
max_tokens: 500,
messages: [{ role: 'user', content: question }]
})
});
const data = await response.json();
return Response.json({ answer: data.choices?.[0]?.message?.content ?? '' });
});In the app, the call becomes supabase.functions.invoke('ask', { body: { question } }), and the Supabase client sends the signed-in user’s token with it.
4. Payments: let the server decide the price
For Stripe, moving the key is not enough. If the browser tells your function what to charge, someone can change the amount. The browser should send only what is being bought (a product or plan ID); the server looks up the price, creates the Checkout Session with the secret key, and returns the link. Then confirm payments through webhooks verified with Stripe’s signature, never from a “success” page the user can open directly.
Before every release
- Search the build for the patterns above.
- Keep secrets out of any variable with a public prefix.
- When the AI suggests putting a key in the frontend “for now”, ask it for a server function instead.
If your app handles payments, AI features or personal data, our free Security Check looks for exposed keys, open functions and unsigned webhooks from the outside, read-only, and sends you the most serious issues in writing.
API keysAI-built appsSupabaseBolt
Keep reading
All articles
App security
Before you launch a Lovable app: a pre-launch security checklist
Fifteen checks to run before real users and real data arrive: database rules, keys, storage, sign-in, payments, functions and backups. Most take minutes.
4 min read

App security
Public Supabase Storage buckets: are your users' files exposed?
Avatars can be public. ID scans, invoices and contracts cannot. How to check your Supabase Storage buckets and rules, and fix them without breaking uploads.
4 min read

App security
Found your Supabase secret key in the browser? What to do in the first hour
A service role or secret key in your frontend gives anyone full access to your database. What to do, in order: revoke, replace, check the logs, fix the cause.
5 min read