Vigia › Help › Security before you launch
Security before you launch
A one-hour checklist for apps built with Lovable, Bolt, Cursor or Claude Code
AI builders get you from idea to a live URL in an afternoon. The same speed makes it easy to ship something you didn't mean to: a secret key in the JavaScript, a database table anyone can read, a .env file sitting in the web root.
This guide covers the checks that matter most before you share your link. Every recommendation links to the official documentation it comes from. Nothing here replaces a security review. It catches the common, cheap-to-fix mistakes. Vigia, which publishes this guide, sells an automated check (described in section 7); you don't need it to follow the guide.
1. Know which keys can be public
Anything the browser downloads, any visitor can read. Some keys are designed for that. Others give full access to your account and must stay on a server you control.
| Key | Public by design? | What the docs say |
|---|---|---|
Supabase publishable key (sb_publishable_…) and legacy anon key |
Yes, if RLS is on | "Safe to expose online", it "only reaches what Row Level Security allows". [Supabase: API keys] |
Supabase secret key (sb_secret_…) and legacy service_role key |
No | "A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control." [Supabase: API keys] |
Stripe publishable key (pk_…) |
Yes | "Only publishable keys are safe to expose outside your application's backend." [Stripe: API keys] |
Stripe secret (sk_…) and restricted (rk_…) keys |
No | Same page: restricted keys must be protected too. [Stripe: API keys] |
| OpenAI API key | No | "Exposing your OpenAI API key in client-side environments like browsers or mobile apps allows malicious users to take that key and make requests on your behalf." Route requests through your own backend. [OpenAI: API key safety, §3] |
Firebase web API key (AIza…) |
Yes | Public by design; access is controlled by Security Rules, IAM and App Check. [Firebase: API keys] |
Watch the environment-variable prefixes. Frameworks copy some variables into the browser bundle on purpose:
- Vite: "Variables prefixed with
VITE_will be exposed in client-side source code after Vite bundling", so "VITE_*variables should not contain sensitive information such as API keys." [Vite: Env variables and modes] - Next.js: a
NEXT_PUBLIC_variable "will be inlined into any JavaScript sent to the browser". Variables without the prefix stay on the server. [Next.js: Environment variables] - Supabase's own
.envexample says it plainly: "Server-only. Never prefix these, or your bundler will ship the key." [Supabase: API keys]
Check it yourself (5 minutes):
- Open your
.envand list everyVITE_orNEXT_PUBLIC_name. Only the "Yes" rows in the table belong there. - Open your live site, then DevTools → Sources, and search all files (Ctrl/Cmd+Shift+F) for
sb_secret_,sk_live_,rk_live_andsk-. - Legacy Supabase keys are long strings starting with
eyJ. Theanonandservice_roleones look alike from the outside, so you need to decode them (or use a check that does) to tell which one shipped. [Supabase: API keys]
Fix: move every call that needs a secret key to server-side code (an API route, a serverless/edge function, or your backend). Vite's docs suggest exactly that: "For production deployments, consider a backend server or serverless/edge functions". [Vite]
Prompt for your AI tool:
Find every secret key used in client-side code (anything with a VITE_ or NEXT_PUBLIC_ prefix, and any hard-coded key). Move each call that needs a secret key to a server-side function, read the key from a server-only environment variable, and leave only publishable keys in the browser.
Is my app safe right now? Run the free check →
2. If a secret key leaked: rotate, in the right order
Removing the key from your code is not enough: everyone who loaded the page may already have a copy. The key itself has to stop working.
Supabase (covers both a leaked secret key and a leaked legacy service_role key) [Supabase: API keys, "Rotate a leaked or compromised key"]:
- "Fix the root cause of the leak before you rotate anything."
- Create a new secret key in Settings → API Keys.
- Replace the compromised key everywhere your application uses it. A legacy
service_rolekey is replaced by the new secret key. - Confirm every component now uses the new key.
- Retire the old key: delete a secret key (this can't be undone), or, for legacy
anon/service_rolekeys, deactivate the legacy keys (reversible).
Good to know: new secret keys return HTTP 401 when used from a browser, but Supabase warns that "an attacker can still use the key from other tools". Delete a leaked key either way.
Stripe: "Rotating an API key revokes it and generates a replacement key that's ready to use immediately." Do it from the API keys page in the Dashboard. [Stripe: API keys]
OpenAI: "If you believe your key has been leaked, rotate your key immediately." [OpenAI: API key safety, §7]
Don't commit secrets. OpenAI advises against committing keys to your repository and recommends environment variables. [OpenAI, §4] Next.js says of .env files: "You almost never want to commit these files to your repository." [Next.js] Vite recommends adding *.local to .gitignore. [Vite]
Check whether a key is still in your live site →
3. Lock your database
Supabase: Row Level Security on every table
The publishable key is public, so your data is protected by the database rules, not by hiding the key.
- "A table in an exposed schema without RLS is readable and writable by any role with a grant on it. Enable RLS on every table in an exposed schema." [Supabase: Row Level Security]
- The Security Advisor flags this as lint 0013: "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled." [Supabase: Database Advisors]
- Once RLS is on, "no data is accessible through the API when using a publishable key, until you create policies". [Supabase: RLS]
Grants come first. "Adding policies doesn't take those grants back. A table protected only by policies still hands anon an insert path if you never revoke the grant." Grant each role only the operations it needs. [Supabase: RLS]
Write one policy per operation (select, insert, update, delete), and watch two traps the docs call out [Supabase: RLS]:
- Logged out,
auth.uid()returns null. Writeauth.uid() IS NOT NULL AND auth.uid() = user_idto make the intent explicit. - Don't base a policy on
user_metadata: it "can be modified by authenticated end users".
Views can skip your policies. In Postgres 15 and above, make a view obey the RLS policies of its tables with security_invoker = true. On older versions, revoke access to the view from anon and authenticated, or keep it in an unexposed schema. [Supabase: RLS] The Advisor flags the risky case as lint 0010, "security definer view". [Supabase: Database Advisors]
Test it like a stranger would. Supabase's procedure: write a .sql file under supabase/tests/ that asserts allow and deny for select, insert, update and delete, for anon and authenticated, then run supabase test db. "Until the suite passes, you don't know whether the policies do what you intended." [Supabase: RLS]
Prompt for your AI tool:
For every table in the public schema: enable RLS, revoke grants that anon and authenticated don't need, write one policy per operation scoped to auth.uid() (with an explicit auth.uid() IS NOT NULL check), recreate every view with (security_invoker = true), and add tests under supabase/tests/ that assert allow and deny for anon and authenticated. Then run supabase test db and the Security Advisor and fix everything they report.
Firebase: Security Rules, not hidden keys
- "Secure your Realtime Database, Cloud Firestore, and Cloud Storage data by using Firebase Security Rules. Don't rely only on restricting and/or obscuring your API keys." [Firebase: API keys]
- When you create a database or bucket you can choose Locked mode or Test mode (access for all users). "Make sure you take the time to properly configure your rules and secure your data before deploying." [Firebase: Avoid insecure rules]
Run the free check (it tells you which Supabase key your site ships) →
4. Keep .env and .git off your web server
- OWASP: make sure
.git,.svnor any other source-control metadata is not deployed alongside the application where it can be reached from outside. And don't store secrets "in clear text or in any non-cryptographically secure manner on the client, in source code, or build artifacts". [OWASP Developer Guide: Secure by default] - OWASP's testing guide lists
.envamong files that "should generally never be publicly served". [OWASP WSTG-CONF-04] - UK Government Security: an exposed
.gitdirectory can let attackers download your source code, including secrets committed by mistake. Block.gitin the web server configuration, deploy only the files you need, delete.gitfrom the live server if it was exposed, and use.gitignoreplus secret management. [UK Gov Security: Git configuration exposure]
Check it yourself (1 minute): open these in a private window, replacing the domain with yours. Each should return "not found", never a file:
https://example.com/.env · /.env.local · /.env.production · /.git/HEAD · /.git/config
If one loads: remove it from the deployment, block the path, and rotate every secret that was in it (section 2).
Check these paths automatically →
5. Cap the damage: spend limits and key restrictions
Even with good hygiene, assume a key may leak one day, and limit what it can cost.
- OpenAI: set monthly spend limits with hard enforcement. "Spend alerts alone don't stop API traffic." OpenAI also describes IP allowlisting and key expiry. [OpenAI: API key safety, §7, §8, §2]
- Stripe: configure access policies so keys can only be used from your known servers, and prefer restricted keys with only the permissions your integration needs. [Stripe: API keys]
- Firebase / Google: never include the Gemini Developer API in the allowlist for a publicly accessible API key. [Firebase: API keys]
- Supabase: you can run a separate secret key per backend component, so one leak only forces one rotation. [Supabase: Migrating to new API keys]
6. The 10-minute pre-launch run
Do this before you post your link anywhere:
- ☐
.env: no secret behind aVITE_/NEXT_PUBLIC_prefix. (§1) - ☐ Live site, DevTools search: no
sb_secret_,sk_live_,rk_live_,sk-. (§1) - ☐ Supabase → Advisors → Security Advisor: zero security warnings. [Supabase: Database Advisors] (§3)
- ☐
supabase test dbpasses for anon and authenticated. (§3) - ☐ Firebase: no rules left in Test mode. (§3)
- ☐
/.env,/.env.local,/.env.production,/.git/HEAD,/.git/configreturn "not found". (§4) - ☐ Hard spend limits on every paid AI API. (§5)
- ☐ If anything leaked at any point: rotated, old key deleted or deactivated. (§2)
7. What Vigia checks (and what it doesn't)
Vigia is a passive, read-only check for apps you own. It looks only at what any visitor can already download.
What it does:
- Reads your home page and up to 6 same-origin JavaScript files linked from it (
<script src>and module preloads; scripts from a CDN or subdomain aren't checked). GET requests only, with a time and size limit. - Requests five paths that should never be public:
/.env,/.env.local,/.env.production,/.git/HEAD,/.git/config. - Flags as critical: OpenAI keys (
sk-,sk-proj-,sk-svcacct-), Anthropic keys (sk-ant-), Stripe live keys (sk_live_,rk_live_), Supabase secret keys (sb_secret_), AWS access key IDs (AKIA…), and Supabase JWTs with theservice_rolerole (it decodes them). - Flags as high: Stripe
rk_test_keys and any other token (JWT) whose purpose it can't tell. It treats those as sensitive. - Notes as info: Supabase publishable keys and
anonJWTs (fine in the browser if RLS is on) and Firebase web keys. - Masks every value it shows (first and last 4 characters at most), including the start of an exposed
.gitfile. Gives a fix prompt for Cursor, Lovable or Bolt. - Asks you to prove you own the site (a token file or meta tag) before it runs the check. Only the browser tab that proved it can run the check, for 24 hours; after that, you prove it again with a new token.
What it doesn't do:
- It does not test your RLS policies or Firebase rules, log in, or run your app.
- It does not read lazily loaded scripts, pages behind login, or source maps.
- It does not detect
sk_test_, GitHub, Slack or SendGrid tokens. - It can't tell whether a key it finds is still active. Rotate it either way.
- A clean report means these common leaks weren't found, not that your app is secure. It is not a security audit or a guarantee.
Price: the one-off check is free. Vigia Pro is €12/month or €120/year (VAT included) for up to 3 apps, with a weekly re-check and email alerts. Cancel anytime; full refund within 14 days.
Run the free check on your app →
— Vigia · Who we are
Sources (opened and checked 24 October 2026)
- Supabase: API keys
- Supabase: Migrating to publishable and secret API keys
- Supabase: Row Level Security
- Supabase: Database Advisors
- Stripe: API keys
- OpenAI Help: Best practices for API key safety
- Vite: Env variables and modes
- Next.js: Environment variables
- Firebase: Learn about using and managing API keys
- Firebase: Avoid insecure rules
- OWASP Developer Guide: Secure by default
- OWASP WSTG-CONF-04: Review old backup and unreferenced files
- UK Government Security: Git configuration exposure
- Vigia's own behaviour: from the checker's source code (internal reference:
worker/src/check.ts; not linked publicly).
Lovable, Bolt, Cursor, Claude Code, Supabase, Stripe, OpenAI, Firebase, Vite and Next.js are trademarks of their respective owners. Vigia is not affiliated with or endorsed by them.