How to Fix Railway API Key Exposure (2026)

A Railway app with a leaked OpenAI key can burn through your API credits within hours. The most common cause is a variable prefixed with VITE_ or NEXT_PUBLIC_: those prefixes tell the build tool to embed the value directly into your client JavaScript, where anyone who opens DevTools can read it.

This guide walks through finding the leak, rotating the key, scrubbing git history if needed, and fixing the root cause so it does not happen again.

TL;DR

Railway API key exposure usually happens in one of three ways: a VITE_SECRET_KEY or NEXT_PUBLIC_SECRET_KEY variable baked into the client bundle, a .env file committed to git, or a secret used as a build argument that ends up in a Docker layer. Rotate the key immediately, then fix the prefix or move the call server-side. Railway's Variables tab is safe for secrets only when you access them from server-side code.

Step 1: Find the Exposed Key

Before rotating, confirm exactly what is exposed and how.

1

Check your JS bundle. Open your deployed app in a browser, press F12, go to the Sources or Network tab, and search for your key value or its known prefix (like sk_live_, AKIA, or sk_proj_). If it appears in a .js file under your domain, it is in your client bundle and visible to anyone.

2

Search your codebase for VITE_ and NEXT_PUBLIC_ variables.

# Find any secret-looking variables with public-facing prefixes
grep -r "VITE_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js"
grep -r "NEXT_PUBLIC_" . --include="*.env*" --include="*.ts" --include="*.tsx"

Any variable named VITE_OPENAI_KEY or NEXT_PUBLIC_STRIPE_SECRET is almost certainly in your bundle.

3

Check git history for committed .env files.

git log --all --full-history -- .env
git log --all --full-history -- .env.local
git log --all --full-history -- .env.production

If any of these return commits, the secrets in those files were stored in your repo's history and may have been visible to anyone with read access.

CheckYourVibe scans your deployed app and flags API keys found in the JavaScript bundle, including OpenAI, Stripe, Supabase service-role, and AWS credentials. It reads what an attacker would read from your public bundle, not your source code.

Step 2: Rotate the Exposed Key Immediately

Do not try to fix the root cause first. The key is already exposed. Rotate it now.

1

Generate a new key in the affected service's dashboard (OpenAI, Stripe, Supabase, etc.). Most services let you have multiple active keys simultaneously so production stays live.

2

Update Railway's Variables tab with the new key value. In the Railway dashboard, click your service, open the Variables tab, find the variable, and update its value.

3

Redeploy. Railway redeploys automatically when you save a variable change. Wait for the new deployment to go live.

4

Revoke the old key in the service dashboard. Do not skip this step. An old key left active is still usable by anyone who copied it.

5

Check usage logs. Review the affected service's usage dashboard for spikes in the period since the key was deployed. If you see unexpected requests, review what endpoints were called and for how long.

Do not delay revocation waiting to "confirm" the key was accessed. Rotate first, investigate second. API abuse can cost thousands of dollars within hours of exposure.

Step 3: Remove from Git History (if committed)

If your audit found a .env file in git history, you need to scrub it. The key is still visible in any clone of the repo even if you deleted the file in a later commit.

# Install git-filter-repo (preferred over filter-branch)
pip install git-filter-repo

# Remove .env from all history
git filter-repo --path .env --invert-paths
git filter-repo --path .env.local --invert-paths

After scrubbing, push with force to all remotes and notify any collaborators to re-clone. If the repo was public at any point, assume the key was copied and rotate regardless.

Also add .env to your .gitignore if it is not already there:

echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore
git add .gitignore
git commit -m "chore: ensure .env files are gitignored"

Step 4: Fix the Root Cause

Rotating is not enough if the underlying pattern sends the key to the client on every build.

The VITE_ and NEXT_PUBLIC_ Problem

Vite embeds every variable prefixed VITE_ into the client bundle at build time. Next.js does the same for NEXT_PUBLIC_. This is by design: these prefixes exist to make values available in browser code.

The fix is to remove the prefix and access the variable only from server-side code.

Wrong (key ends up in browser bundle):

// vite app: this sends OPENAI_API_KEY to every visitor's browser
const client = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_API_KEY });

Right (key stays server-side via an API route):

// server/api/chat.post.ts (Nuxt) or pages/api/chat.ts (Next.js)
// OPENAI_API_KEY is accessed here, server only, never in the browser bundle
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const response = await client.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: body.message }],
  });
  return response.choices[0].message.content;
});

The client code calls /api/chat on your own domain. Your server calls OpenAI. The key never leaves your server.

Railway Variable Naming

In Railway's Variables tab, name your secret without any public prefix:

WrongRight
VITE_OPENAI_API_KEYOPENAI_API_KEY
NEXT_PUBLIC_STRIPE_SECRETSTRIPE_SECRET_KEY
VITE_DATABASE_URLDATABASE_URL

Then update your server-side code to use process.env.OPENAI_API_KEY. Railway injects it into the runtime environment where only your server code can access it.

Step 5: Use Railway Private Networking for Internal Services

If your Railway project has multiple services (for example, a web app and a PostgreSQL database), connect them using Railway's private network instead of the public URL.

Private URLs end in .railway.internal and only work within your project. The database is not reachable from the public internet at all.

In your Railway Variables tab, set:

DATABASE_URL=postgresql://postgres:password@postgres.railway.internal:5432/railway

Instead of the public URL that looks like:

DATABASE_URL=postgresql://postgres:password@containers-us-west-XX.railway.app:XXXXX/railway

With private networking, even if someone extracted your DATABASE_URL from source code, they could not connect to the database from outside Railway's network.

Find your service's private hostname in Railway by clicking the service, going to the Settings tab, and checking the "Private Networking" section. It is only available when you have multiple services in the same project.

Step 6: Prevent Future Leaks

Pre-commit Hook with Gitleaks

Gitleaks scans for secrets before each commit:

# Install gitleaks (macOS)
brew install gitleaks

# Test your current repo
gitleaks detect --source . --verbose

# Add as pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
  echo "Gitleaks found potential secrets. Commit blocked."
  exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

Railway Build Argument Warning

If you are using Docker on Railway and passing secrets as ARG values in your Dockerfile, those values are stored in the Docker layer cache and can be extracted from the image. Use Railway's runtime Variables tab instead of build arguments for secrets.

# Wrong: secret visible in image layers
ARG OPENAI_API_KEY
ENV OPENAI_API_KEY=$OPENAI_API_KEY

# Right: access process.env.OPENAI_API_KEY at runtime
# No ARG/ENV for secrets in Dockerfile

How do I know if my Railway API key is exposed?

Open your deployed app in the browser, press F12, go to Sources, and search for your key value or its prefix (like sk_live_ or AKIA). If it appears, it is in your client bundle and visible to anyone. Also check: run git log --all --full-history -- .env to see if your .env file was ever committed to git history.

What's the difference between Railway Variables and .env files?

Railway Variables are stored server-side in Railway's encrypted secrets manager and injected into your service at runtime. They never touch your codebase. A .env file is a local file that should never be committed to git. Put secrets in Railway's Variables tab, not in .env files you deploy.

Why does VITE_OPENAI_API_KEY expose my key even though it's in Railway?

Vite bakes any variable prefixed with VITE_ into your client-side JavaScript bundle at build time. Even if the variable lives safely in Railway's Variables tab during the build, it ends up as plain text in your deployed JS files. Rename it to OPENAI_API_KEY (no VITE_ prefix) and access it only in your server-side code or API routes.

Do I need to rotate my key if I'm not sure it was accessed?

Yes. Rotate it anyway. It takes under 5 minutes and the cost of not rotating a compromised key is far higher than updating a valid one. Check the service's usage dashboard for unexpected spikes after the potential exposure window.

Can Railway Private Networking protect my database URL?

Yes. When all your services are in the same Railway project, connect them using their private hostname (like postgres.railway.internal) instead of the public URL. Private traffic stays inside Railway's network and never crosses the public internet.

Check your Railway deployment for API keys in your JavaScript bundle, exposed environment variables, and security header gaps. Free scan, no signup required.

How-To Guides

How to Fix Railway API Key Exposure (2026)