KeyDrift
Free scan
criticalReplit·Supabase JWT

Replit Secrets and the supabase jwt that ended up in your bundle

Full read and write access to every table, bypassing Row Level Security.

Why Replit does this

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.

Confirm it first

Before rotating anything, check whether the key is actually being served. Paste your deployed URL — KeyDrift downloads the same JavaScript a visitor gets and tells you what is in it.

No account. Read-only — the scanner only ever issues GET requests, and never stores a key: findings carry a masked prefix and a fingerprint.

Rotate the key

Do this before changing any code. The key has been served to browsers, cached by CDNs and very likely scraped already — removing it from the source does not un-publish it.

  1. 1Rotate the key in Project Settings → API. The old one stops working immediately.
  2. 2Move whatever needed it into an Edge Function or a server route, and keep only the anon key in the browser.
  3. 3Check Row Level Security is enabled on every table — a leaked service_role key bypasses it, so RLS is what limits the damage from the next one.
Open the revocation page

Move the call to a server

The replacement key must not follow the old one into the bundle, which means the code that uses it cannot live in the browser. Any variable named VITE_ is inlined at build time by design — the prefix is the mechanism, not a mistake.

Before — shipped to the browser

// src/components/Chat.tsx
// Vite substitutes the literal value here at build time.
const key = import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY;
const result = await createClient(url, key).from(table).select();

After — stays on the Express server in the same Repl

// supabase/functions/admin/index.ts  — runs on the Express server in the same Repl
Deno.serve(async (request) => {
  const key = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; // never sent to the browser
  const result = await createClient(url, key).from(table).select();
  return Response.json(result);
});

// src/components/Chat.tsx
const result = await fetch('/functions/v1/admin', { method: 'POST' }).then((r) => r.json());

How KeyDrift detects it

Matches any three-segment JWT, then decodes the payload — without verifying the signature, because the question is what the token claims to be, not whether it is valid. A `role` claim of `service_role` is critical; `anon` and `authenticated` belong in a browser and are reported as informational. A token issued by `supabase-demo` is the local development default that `supabase start` generates identically on every machine, so it is not treated as a leak.

It will happen again

Replit Secrets are injected as environment variables at build time. That has not changed because you fixed this one file — the next feature request produces the same shape of code. Continuous monitoring re-scans every deploy and tells you the moment a key comes back.