Your AI just put a secret key in the browser.
Find what your deployed app is actually shipping.
Next.js on Netlify or Vercel inlines your build-time env into the bundle every visitor downloads. KeyDrift reads the JavaScript your app actually serves — not your repository — and finds the Supabase, Stripe, OpenAI and AWS keys that should never have left your server. Free, no account — and it ignores the anon key and the publishable Stripe key on purpose, because those belong there.
A real scan of our own demo page
"use strict";const CONFIG={apiVersion:"v1",env:"demo",projectRef:"xyzdemo",region:"us-east-1"};const supabaseUrl="https://xyzdemo.supabase.co";const supabaseAnonKey="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5emRlbW8iLCJyb2xlIjoiYW5vbiIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxOTAwMDAwMDAwfQ.2DKu9b4fu1AFk_Jf4sngXCFBEnshbqSgWXIp5_IrpSE";const supabaseServiceKey="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5emRlbW8iLCJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE5MDAwMDAwMDB9.2BDIn8Kq_ja3u1Xn9fGdM3j5Ac8rWkoIKjdsGzEZemw";const REST_PATH="/rest/v1/";function authHeaders(key){return{apikey:key,Authorization:"Bearer "+key,"Content-Type":"application/json"};}function endpoint(table){return supabaseUrl+REST_PATH+table;}function withTimeout(promise,ms){return Promise.race([promise,new Promise(function(_,reject){setTimeout(function(){reject(new Error("timeout"));},ms);})]);}async function fetchTable(table,key){const res=await withTimeout(fetch(endpoint(table),{headers:authHeaders(key)}),5000);if(!res.ok)throw new Error("fetch failed: "+res.status);return res.json();}function maskKey(key){return key.slice(0,8)+"..."+key.slice(-4);}function log(label,value){console.log("[demo-target]",label,value);}function debounce(fn,wait){var t;return function(){var args=arguments;clearTimeout(t);t=setTimeout(function(){fn.apply(null,args);},wait);};}async function loadPublicProfiles(){const rows=await fetchTable("profiles",supabaseAnonKey);log("profiles (anon)",rows.length);return rows;}async function adminExport(table){const rows=await fetchTable(table,supabaseServiceKey);log("admin export",table);return rows;}function renderList(el,rows){if(!el)return;el.innerHTML=rows.map(function(r){return "<li>"+JSON.stringify(r)+"</li>";}).join("");}function boot(){var el=document.getElementById("demo-rows");loadPublicProfiles().then(function(rows){renderList(el,rows);}).catch(function(err){log("error",err.message);});}function adminPanel(){var out=document.getElementById("admin-out");adminExport("profiles").then(function(rows){if(out)out.textContent=JSON.stringify(rows.length)+" rows (service_role)";});}function retry(fn,times){return fn().catch(function(err){if(times<=0)throw err;return retry(fn,times-1);});}function refreshAll(){retry(function(){return loadPublicProfiles();},2);}var refreshDebounced=debounce(refreshAll,250);window.demoTarget={boot:boot,adminPanel:adminPanel,maskKey:maskKey,config:CONFIG,refresh:refreshDebounced};window.addEventListener("online",refreshDebounced);document.addEventListener("DOMContentLoaded",boot);
eyJhbGci…ZemwPublic assets only
Fetches deployed JavaScript, never your private infrastructure.
No credentials required
Start with a URL or paste a bundle directly into the scanner.
Live keys never stored
Findings are masked and fingerprinted instead of retaining secrets.
Free first scan
No account is needed to find out what your app is shipping.
21
secret detectors
3
public credential formats, recognised so they are never reported as leaks
0
live keys stored, ever — findings carry a masked prefix and a fingerprint
Monitoring plans
The public scan is free. Monitoring starts when you turn it on.
Paid access begins when a customer enables continuous monitoring. Every paid plan includes redacted findings, and live secrets are never stored.
KeyDrift Free
$0
Find out what your deployed app is shipping.
- 1 project
- Daily scans
- Email alerts
- 40 chunks per scan
KeyDrift Indie
$29 / monthor $290/yr
Continuous client-bundle secret monitoring for up to 3 applications.
- Hourly scans
- Email and Slack alerts
- Up to 80 chunks per scan
- Redacted findings
- Live secrets are never stored
KeyDrift Team
Most coverage$89 / monthor $890/yr
Continuous client-bundle secret monitoring for up to 15 applications.
- 15-minute scans
- Email, Slack, Discord and webhook alerts
- Up to 150 chunks per scan
- Redacted findings
- Live secrets are never stored
- Cancel anytime
- Human support (+47 988 36 369 / support@teamveristria.com)
- Refunds if unused or broken (see Terms)
- Reply within one business day
- Evidence you can check yourself
- We only email from @teamveristria.com, @useveristria.com, @meetveristria.com, @veristriagroup.com, @veristriahq.com, @veristriaworks.com and @withveristria.com
- Live secrets never stored
- Read-only public checks on Free
Get product updates
Leave your email for release notes and pricing updates — the free tier is available today.
Founding customers (first 100) lock in 25% off annual forever.
KeyDrift Growth
customfrom $249 / month
For platforms that need more coverage than Team. Quoted per deployment.
Most scanners read your repository. The key was never in your repository.
The build is what leaks
NEXT_PUBLIC_ and VITE_ substitute the literal value into the bundle at build time. Your .env is correctly git-ignored and the key still ships to every visitor. A repository scanner sees nothing wrong because nothing is wrong — in the repository.
Knowing what is supposed to be there
A client bundle is meant to contain a Supabase anon key, a Stripe publishable key and a Firebase web key. A scanner that flags those has told you your working app is on fire. KeyDrift recognises them and excludes them — which is also what lets it say, with confidence, that the JWT next to them is a service_role key.
Built with Lovable, Bolt, Cursor, Replit or v0? Your builder may have put a key in the browser.
Lovable
Lovable builds a Vite single-page app with no server of its own. When you ask it to call an API that needs a key, the only place it can put that key is the browser — so it adds a `VITE_`-prefixed variable, and Vite substitutes the literal value into the bundle at build time.
Fix guides →Bolt.new
Bolt scaffolds Vite projects inside a WebContainer, where everything runs in the browser by definition. Code that works in the preview keeps the key client-side when it is deployed, because nothing in the generated project ever moved it to a server.
Fix guides →Cursor
Cursor edits the file you have open. Ask it to "call the OpenAI API" from a component and it writes exactly that — a client component holding a key — because the request named a file, not an architecture. The agent has no way to know the module is bundled for the browser.
Fix guides →Claude Code
A CLI agent working across the repo will follow the pattern it already sees. If one component reads a key from `import.meta.env`, the next feature it writes will too — the leak spreads by imitation rather than by a single mistake.
Fix guides →Replit
Replit Secrets are injected as environment variables at build time. A secret that gets a `VITE_` prefix so the frontend can "see" it stops being a secret — the prefix is the mechanism that copies it into the bundle.
Fix guides →Next.js
A server component can read `process.env.STRIPE_SECRET_KEY` safely. Move that same line into a client component and the build fails to find it, so the quickest fix — rename it with `NEXT_PUBLIC_` — is also the one that inlines it into the JavaScript every visitor downloads.
Fix guides →Rotating fixes today. Monitoring catches tomorrow.
The prompt that wrote this code is still in your editor, and it will write it again. Point KeyDrift at a deploy and it re-scans on every push, alerting only when something actually changes.
$29 / month Indie · $89 / month Team. Findings are never withheld on any plan.
Questions we get asked
- Does the free scan check for a Supabase service_role key?
- Yes. It is the first rule in the catalogue and the most common serious finding; the anon key beside it is recognised and ignored on purpose.
- Aren't NEXT_PUBLIC_ variables supposed to be public?
- Yes, and that is the mechanism: anything with that prefix is inlined into the bundle at build time. The scan tells you which of those values should never have been public.
- Can I just check the Network tab myself?
- Yes. Open DevTools, search the loaded JavaScript for "sk_", "service_role" or "eyJ". The scan does the same across every chunk, including the ones that load later, and tells you which matches are harmless.