Skip to content
KeyDrift
Scan for free

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.

No account. Read-only — the scanner only ever issues GET requests, and never stores a key: findings carry a masked prefix and a fingerprint. The report is kept at a private link so you can share it.

Free scanIgnores anon keys on purpose

A real scan of our own demo page

keydrift.dev/demo-target · run it yourself
"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);
service_role key in the browser ← finding
supabase-jwtdemo-target/app.js:5eyJhbGci…Zemw
Open the real report

Public 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
Scan for free

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

custom

from $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 →
Continuous protection

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.

See monitoring plans

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.