ShipWatchFree scan

Replit Secrets aren't safe if your frontend reads them

Replit's Secrets tool encrypts your keys and hands them to your app as environment variables. That protects the key in your project, not in your published site. If client code reads the secret, Replit will happily ship it to every visitor.

Short answer: A Replit Secret is only private while server code uses it. It leaks when the agent puts it in a VITE_ or NEXT_PUBLIC_ variable, hardcodes it into a frontend file, or returns it from an API route. Keep secret calls on your Express, Flask or other server and have the browser call that server.

This guide is the Replit-specific follow-up to the Bolt, v0 and Replit overview. It assumes your app has a frontend and some server code in the same Repl, which is what Replit Agent usually builds.

How Replit Secrets work

You add a secret in the Secrets tool (Tools, then Secrets). Replit encrypts it and exposes it to your project as an environment variable, so a Node server reads process.env.OPENAI_API_KEY and Python reads os.environ["OPENAI_API_KEY"]. Published apps use the production secrets listed under Publishing settings, which can differ from the ones you test with.

That model is safe for code that runs on Replit's servers. The browser never sees your environment. The problem is code that copies the value somewhere the browser can see.

The three ways a Secret leaks

  1. Bundler prefix. Many Repls use Vite for the frontend. When the agent hits "undefined" reading a secret in a React component, it may rename the secret to VITE_OPENAI_API_KEY. Vite then writes the value into your built JavaScript.
  2. Hardcoding. The agent pastes the value straight into client/src/lib/api.ts to "make it work". Now it's in your code, your bundle and possibly your git history.
  3. Server passes it through. An endpoint like /api/config returns { openaiKey: process.env.OPENAI_API_KEY } so the frontend can call OpenAI directly. The key is server-side in name only.

A fourth case: a Static Deployment. Replit's docs say Secrets aren't available there at all, so a static app that calls a paid API must carry the key in its files.

Check client vs. server [manual]

In the Repl's Shell, search the frontend for anything secret-looking:

grep -rnE "VITE_|NEXT_PUBLIC_|sk-proj-|sk_live_|sk-ant-|sb_secret_" client/ src/ 2>/dev/null
grep -rn "process.env" server/ | grep -i "res.json\|send("

The first finds prefixed variables and pasted keys in client code. The second catches server routes that send env values back. Folder names vary by template, so adjust the paths. Then open your published .replit.app URL, open DevTools, and search the loaded scripts in Sources for the first characters of each key.

How to fix it

1. Rotate first [manual]

If a key reached the published bundle, treat it as stolen. Revoke it with the provider, create a new one, and update it in both the Secrets tool and the production app secrets. The rotate-first playbook lists where each provider's button is.

2. Proxy the call through your server

Replace the browser's direct call with a route on your own server. In Express:

app.post("/api/chat", requireUser, async (req, res) => {
  const r = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "gpt-4o-mini", messages: req.body.messages }),
  });
  res.status(r.status).json(await r.json());
});

requireUser stands for whatever login check your app has. Without it, your route becomes a free API for strangers.

Audit this Repl for secrets reaching the browser. Find any secret used in client code, any VITE_ or NEXT_PUBLIC_ variable holding a key, and any server route that returns an environment variable. Move each secret call into a server route that reads the key from process.env (Replit Secrets), require a logged-in user and rate limit it, update the frontend to call that route, and remove the key from all client files.

3. Republish

Changes reach your .replit.app site only after you publish again. Rescan after it's live.

Make it stick

What the free scan checks here (and what it doesn't)

The scan reads your published HTML plus up to 25 linked scripts and flags Stripe sk_live_/rk_live_, OpenAI, Anthropic, AWS, Google AIza, and Supabase service_role and sb_secret_ keys. It reports a Supabase anon key or URL as info only. It also checks /.env, /.git/HEAD, HTTPS and its redirect, CSP, HSTS, X-Frame-Options or frame-ancestors, and nosniff.

It can't see a route like /api/config that returns a key on request, because it only loads your page and its scripts. The grep above covers that [manual].

FAQ

Can visitors to my public Replit project see my Secrets?
According to Replit's docs, cover page visitors can't see secret names or values, and people who remix a project they don't own see the names but not the values. The bigger risk is your own code sending the value to the browser.
Do Secrets work with a Replit Static Deployment?
No. Replit's docs say Secrets are available for all deployment types except Static Deployments. A static site has no server to hold them, so any key it uses ends up in the files you publish.
What does the free scan find on a replit.app site?
It reads your published page and up to 25 linked scripts and flags Stripe, OpenAI, Anthropic, AWS, Google and Supabase secret keys, and checks /.env, /.git/HEAD, HTTPS and four security headers. It doesn't read your Repl's server code.

Scan your app free in 30 seconds

Paste your live URL for a plain-English report with fix prompts. If you keep shipping, weekly monitoring re-checks it: Solo $19/mo or Pro $49/mo.

Scan my app free