[{"data":1,"prerenderedAt":989},["ShallowReactive",2],{"blog-how-to/fix-railway-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":959,"date":960,"dateModified":960,"description":961,"draft":962,"extension":963,"faq":964,"featured":962,"headerVariant":974,"image":975,"keywords":976,"meta":977,"navigation":339,"ogDescription":978,"ogTitle":975,"path":979,"readTime":980,"schemaOrg":981,"schemaType":982,"seo":983,"sitemap":984,"stem":985,"tags":986,"twitterCard":987,"__hash__":988},"blog/blog/how-to/fix-railway-api-key-exposure.md","How to Fix Railway API Key Exposure (2026)",{"type":7,"value":8,"toc":945},"minimark",[9,22,25,42,47,50,77,177,243,249,253,256,264,272,280,289,298,304,308,314,378,381,391,455,459,462,467,476,479,484,501,506,576,583,587,590,645,652,656,659,666,669,677,680,686,692,697,701,705,708,829,833,840,876,914,933,941],[10,11,12,13,17,18,21],"p",{},"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 ",[14,15,16],"code",{},"VITE_"," or ",[14,19,20],{},"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.",[10,23,24],{},"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.",[26,27,28],"tldr",{},[10,29,30,31,17,34,37,38,41],{},"Railway API key exposure usually happens in one of three ways: a ",[14,32,33],{},"VITE_SECRET_KEY",[14,35,36],{},"NEXT_PUBLIC_SECRET_KEY"," variable baked into the client bundle, a ",[14,39,40],{},".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.",[43,44,46],"h2",{"id":45},"step-1-find-the-exposed-key","Step 1: Find the Exposed Key",[10,48,49],{},"Before rotating, confirm exactly what is exposed and how.",[51,52,54],"step",{"number":53},"1",[10,55,56,60,61,64,65,68,69,72,73,76],{},[57,58,59],"strong",{},"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 ",[14,62,63],{},"sk_live_",", ",[14,66,67],{},"AKIA",", or ",[14,70,71],{},"sk_proj_","). If it appears in a ",[14,74,75],{},".js"," file under your domain, it is in your client bundle and visible to anyone.",[51,78,80,85,167],{"number":79},"2",[10,81,82],{},[57,83,84],{},"Search your codebase for VITE_ and NEXT_PUBLIC_ variables.",[86,87,92],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":91},"language-bash shiki shiki-themes github-light github-dark","# Find any secret-looking variables with public-facing prefixes\ngrep -r \"VITE_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\ngrep -r \"NEXT_PUBLIC_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\"\n","bash","",[14,93,94,103,142],{"__ignoreMap":91},[95,96,99],"span",{"class":97,"line":98},"line",1,[95,100,102],{"class":101},"sJ8bj","# Find any secret-looking variables with public-facing prefixes\n",[95,104,106,110,114,118,121,124,127,129,132,134,137,139],{"class":97,"line":105},2,[95,107,109],{"class":108},"sScJk","grep",[95,111,113],{"class":112},"sj4cs"," -r",[95,115,117],{"class":116},"sZZnC"," \"VITE_\"",[95,119,120],{"class":116}," .",[95,122,123],{"class":112}," --include=",[95,125,126],{"class":116},"\"*.env*\"",[95,128,123],{"class":112},[95,130,131],{"class":116},"\"*.ts\"",[95,133,123],{"class":112},[95,135,136],{"class":116},"\"*.tsx\"",[95,138,123],{"class":112},[95,140,141],{"class":116},"\"*.js\"\n",[95,143,145,147,149,152,154,156,158,160,162,164],{"class":97,"line":144},3,[95,146,109],{"class":108},[95,148,113],{"class":112},[95,150,151],{"class":116}," \"NEXT_PUBLIC_\"",[95,153,120],{"class":116},[95,155,123],{"class":112},[95,157,126],{"class":116},[95,159,123],{"class":112},[95,161,131],{"class":116},[95,163,123],{"class":112},[95,165,166],{"class":116},"\"*.tsx\"\n",[10,168,169,170,17,173,176],{},"Any variable named ",[14,171,172],{},"VITE_OPENAI_KEY",[14,174,175],{},"NEXT_PUBLIC_STRIPE_SECRET"," is almost certainly in your bundle.",[51,178,180,185,240],{"number":179},"3",[10,181,182],{},[57,183,184],{},"Check git history for committed .env files.",[86,186,188],{"className":88,"code":187,"language":90,"meta":91,"style":91},"git log --all --full-history -- .env\ngit log --all --full-history -- .env.local\ngit log --all --full-history -- .env.production\n",[14,189,190,210,225],{"__ignoreMap":91},[95,191,192,195,198,201,204,207],{"class":97,"line":98},[95,193,194],{"class":108},"git",[95,196,197],{"class":116}," log",[95,199,200],{"class":112}," --all",[95,202,203],{"class":112}," --full-history",[95,205,206],{"class":112}," --",[95,208,209],{"class":116}," .env\n",[95,211,212,214,216,218,220,222],{"class":97,"line":105},[95,213,194],{"class":108},[95,215,197],{"class":116},[95,217,200],{"class":112},[95,219,203],{"class":112},[95,221,206],{"class":112},[95,223,224],{"class":116}," .env.local\n",[95,226,227,229,231,233,235,237],{"class":97,"line":144},[95,228,194],{"class":108},[95,230,197],{"class":116},[95,232,200],{"class":112},[95,234,203],{"class":112},[95,236,206],{"class":112},[95,238,239],{"class":116}," .env.production\n",[10,241,242],{},"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.",[244,245,246],"tip-box",{},[10,247,248],{},"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.",[43,250,252],{"id":251},"step-2-rotate-the-exposed-key-immediately","Step 2: Rotate the Exposed Key Immediately",[10,254,255],{},"Do not try to fix the root cause first. The key is already exposed. Rotate it now.",[51,257,258],{"number":53},[10,259,260,263],{},[57,261,262],{},"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.",[51,265,266],{"number":79},[10,267,268,271],{},[57,269,270],{},"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.",[51,273,274],{"number":179},[10,275,276,279],{},[57,277,278],{},"Redeploy."," Railway redeploys automatically when you save a variable change. Wait for the new deployment to go live.",[51,281,283],{"number":282},"4",[10,284,285,288],{},[57,286,287],{},"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.",[51,290,292],{"number":291},"5",[10,293,294,297],{},[57,295,296],{},"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.",[299,300,301],"danger-box",{},[10,302,303],{},"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.",[43,305,307],{"id":306},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[10,309,310,311,313],{},"If your audit found a ",[14,312,40],{}," 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.",[86,315,317],{"className":88,"code":316,"language":90,"meta":91,"style":91},"# Install git-filter-repo (preferred over filter-branch)\npip install git-filter-repo\n\n# Remove .env from all history\ngit filter-repo --path .env --invert-paths\ngit filter-repo --path .env.local --invert-paths\n",[14,318,319,324,335,341,347,364],{"__ignoreMap":91},[95,320,321],{"class":97,"line":98},[95,322,323],{"class":101},"# Install git-filter-repo (preferred over filter-branch)\n",[95,325,326,329,332],{"class":97,"line":105},[95,327,328],{"class":108},"pip",[95,330,331],{"class":116}," install",[95,333,334],{"class":116}," git-filter-repo\n",[95,336,337],{"class":97,"line":144},[95,338,340],{"emptyLinePlaceholder":339},true,"\n",[95,342,344],{"class":97,"line":343},4,[95,345,346],{"class":101},"# Remove .env from all history\n",[95,348,350,352,355,358,361],{"class":97,"line":349},5,[95,351,194],{"class":108},[95,353,354],{"class":116}," filter-repo",[95,356,357],{"class":112}," --path",[95,359,360],{"class":116}," .env",[95,362,363],{"class":112}," --invert-paths\n",[95,365,367,369,371,373,376],{"class":97,"line":366},6,[95,368,194],{"class":108},[95,370,354],{"class":116},[95,372,357],{"class":112},[95,374,375],{"class":116}," .env.local",[95,377,363],{"class":112},[10,379,380],{},"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.",[10,382,383,384,386,387,390],{},"Also add ",[14,385,40],{}," to your ",[14,388,389],{},".gitignore"," if it is not already there:",[86,392,394],{"className":88,"code":393,"language":90,"meta":91,"style":91},"echo \".env\" >> .gitignore\necho \".env.local\" >> .gitignore\necho \".env.*.local\" >> .gitignore\ngit add .gitignore\ngit commit -m \"chore: ensure .env files are gitignored\"\n",[14,395,396,411,422,433,442],{"__ignoreMap":91},[95,397,398,401,404,408],{"class":97,"line":98},[95,399,400],{"class":112},"echo",[95,402,403],{"class":116}," \".env\"",[95,405,407],{"class":406},"szBVR"," >>",[95,409,410],{"class":116}," .gitignore\n",[95,412,413,415,418,420],{"class":97,"line":105},[95,414,400],{"class":112},[95,416,417],{"class":116}," \".env.local\"",[95,419,407],{"class":406},[95,421,410],{"class":116},[95,423,424,426,429,431],{"class":97,"line":144},[95,425,400],{"class":112},[95,427,428],{"class":116}," \".env.*.local\"",[95,430,407],{"class":406},[95,432,410],{"class":116},[95,434,435,437,440],{"class":97,"line":343},[95,436,194],{"class":108},[95,438,439],{"class":116}," add",[95,441,410],{"class":116},[95,443,444,446,449,452],{"class":97,"line":349},[95,445,194],{"class":108},[95,447,448],{"class":116}," commit",[95,450,451],{"class":112}," -m",[95,453,454],{"class":116}," \"chore: ensure .env files are gitignored\"\n",[43,456,458],{"id":457},"step-4-fix-the-root-cause","Step 4: Fix the Root Cause",[10,460,461],{},"Rotating is not enough if the underlying pattern sends the key to the client on every build.",[463,464,466],"h3",{"id":465},"the-vite_-and-next_public_-problem","The VITE_ and NEXT_PUBLIC_ Problem",[10,468,469,470,472,473,475],{},"Vite embeds every variable prefixed ",[14,471,16],{}," into the client bundle at build time. Next.js does the same for ",[14,474,20],{},". This is by design: these prefixes exist to make values available in browser code.",[10,477,478],{},"The fix is to remove the prefix and access the variable only from server-side code.",[10,480,481],{},[57,482,483],{},"Wrong (key ends up in browser bundle):",[86,485,489],{"className":486,"code":487,"language":488,"meta":91,"style":91},"language-javascript shiki shiki-themes github-light github-dark","// vite app: this sends OPENAI_API_KEY to every visitor's browser\nconst client = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_API_KEY });\n","javascript",[14,490,491,496],{"__ignoreMap":91},[95,492,493],{"class":97,"line":98},[95,494,495],{},"// vite app: this sends OPENAI_API_KEY to every visitor's browser\n",[95,497,498],{"class":97,"line":105},[95,499,500],{},"const client = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_API_KEY });\n",[10,502,503],{},[57,504,505],{},"Right (key stays server-side via an API route):",[86,507,509],{"className":486,"code":508,"language":488,"meta":91,"style":91},"// server/api/chat.post.ts (Nuxt) or pages/api/chat.ts (Next.js)\n// OPENAI_API_KEY is accessed here, server only, never in the browser bundle\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport default defineEventHandler(async (event) => {\n  const body = await readBody(event);\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: body.message }],\n  });\n  return response.choices[0].message.content;\n});\n",[14,510,511,516,521,526,530,535,540,546,552,558,564,570],{"__ignoreMap":91},[95,512,513],{"class":97,"line":98},[95,514,515],{},"// server/api/chat.post.ts (Nuxt) or pages/api/chat.ts (Next.js)\n",[95,517,518],{"class":97,"line":105},[95,519,520],{},"// OPENAI_API_KEY is accessed here, server only, never in the browser bundle\n",[95,522,523],{"class":97,"line":144},[95,524,525],{},"const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n",[95,527,528],{"class":97,"line":343},[95,529,340],{"emptyLinePlaceholder":339},[95,531,532],{"class":97,"line":349},[95,533,534],{},"export default defineEventHandler(async (event) => {\n",[95,536,537],{"class":97,"line":366},[95,538,539],{},"  const body = await readBody(event);\n",[95,541,543],{"class":97,"line":542},7,[95,544,545],{},"  const response = await client.chat.completions.create({\n",[95,547,549],{"class":97,"line":548},8,[95,550,551],{},"    model: \"gpt-4o\",\n",[95,553,555],{"class":97,"line":554},9,[95,556,557],{},"    messages: [{ role: \"user\", content: body.message }],\n",[95,559,561],{"class":97,"line":560},10,[95,562,563],{},"  });\n",[95,565,567],{"class":97,"line":566},11,[95,568,569],{},"  return response.choices[0].message.content;\n",[95,571,573],{"class":97,"line":572},12,[95,574,575],{},"});\n",[10,577,578,579,582],{},"The client code calls ",[14,580,581],{},"/api/chat"," on your own domain. Your server calls OpenAI. The key never leaves your server.",[463,584,586],{"id":585},"railway-variable-naming","Railway Variable Naming",[10,588,589],{},"In Railway's Variables tab, name your secret without any public prefix:",[591,592,593,606],"table",{},[594,595,596],"thead",{},[597,598,599,603],"tr",{},[600,601,602],"th",{},"Wrong",[600,604,605],{},"Right",[607,608,609,622,633],"tbody",{},[597,610,611,617],{},[612,613,614],"td",{},[14,615,616],{},"VITE_OPENAI_API_KEY",[612,618,619],{},[14,620,621],{},"OPENAI_API_KEY",[597,623,624,628],{},[612,625,626],{},[14,627,175],{},[612,629,630],{},[14,631,632],{},"STRIPE_SECRET_KEY",[597,634,635,640],{},[612,636,637],{},[14,638,639],{},"VITE_DATABASE_URL",[612,641,642],{},[14,643,644],{},"DATABASE_URL",[10,646,647,648,651],{},"Then update your server-side code to use ",[14,649,650],{},"process.env.OPENAI_API_KEY",". Railway injects it into the runtime environment where only your server code can access it.",[43,653,655],{"id":654},"step-5-use-railway-private-networking-for-internal-services","Step 5: Use Railway Private Networking for Internal Services",[10,657,658],{},"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.",[10,660,661,662,665],{},"Private URLs end in ",[14,663,664],{},".railway.internal"," and only work within your project. The database is not reachable from the public internet at all.",[10,667,668],{},"In your Railway Variables tab, set:",[86,670,675],{"className":671,"code":673,"language":674},[672],"language-text","DATABASE_URL=postgresql://postgres:password@postgres.railway.internal:5432/railway\n","text",[14,676,673],{"__ignoreMap":91},[10,678,679],{},"Instead of the public URL that looks like:",[86,681,684],{"className":682,"code":683,"language":674},[672],"DATABASE_URL=postgresql://postgres:password@containers-us-west-XX.railway.app:XXXXX/railway\n",[14,685,683],{"__ignoreMap":91},[10,687,688,689,691],{},"With private networking, even if someone extracted your ",[14,690,644],{}," from source code, they could not connect to the database from outside Railway's network.",[244,693,694],{},[10,695,696],{},"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.",[43,698,700],{"id":699},"step-6-prevent-future-leaks","Step 6: Prevent Future Leaks",[463,702,704],{"id":703},"pre-commit-hook-with-gitleaks","Pre-commit Hook with Gitleaks",[10,706,707],{},"Gitleaks scans for secrets before each commit:",[86,709,711],{"className":88,"code":710,"language":90,"meta":91,"style":91},"# Install gitleaks (macOS)\nbrew install gitleaks\n\n# Test your current repo\ngitleaks detect --source . --verbose\n\n# Add as pre-commit hook\ncat > .git/hooks/pre-commit \u003C\u003C 'EOF'\n#!/bin/sh\ngitleaks protect --staged --verbose\nif [ $? -ne 0 ]; then\n  echo \"Gitleaks found potential secrets. Commit blocked.\"\n  exit 1\nfi\nEOF\nchmod +x .git/hooks/pre-commit\n",[14,712,713,718,728,732,737,753,757,762,779,784,789,794,799,805,811,817],{"__ignoreMap":91},[95,714,715],{"class":97,"line":98},[95,716,717],{"class":101},"# Install gitleaks (macOS)\n",[95,719,720,723,725],{"class":97,"line":105},[95,721,722],{"class":108},"brew",[95,724,331],{"class":116},[95,726,727],{"class":116}," gitleaks\n",[95,729,730],{"class":97,"line":144},[95,731,340],{"emptyLinePlaceholder":339},[95,733,734],{"class":97,"line":343},[95,735,736],{"class":101},"# Test your current repo\n",[95,738,739,742,745,748,750],{"class":97,"line":349},[95,740,741],{"class":108},"gitleaks",[95,743,744],{"class":116}," detect",[95,746,747],{"class":112}," --source",[95,749,120],{"class":116},[95,751,752],{"class":112}," --verbose\n",[95,754,755],{"class":97,"line":366},[95,756,340],{"emptyLinePlaceholder":339},[95,758,759],{"class":97,"line":542},[95,760,761],{"class":101},"# Add as pre-commit hook\n",[95,763,764,767,770,773,776],{"class":97,"line":548},[95,765,766],{"class":108},"cat",[95,768,769],{"class":406}," >",[95,771,772],{"class":116}," .git/hooks/pre-commit",[95,774,775],{"class":406}," \u003C\u003C",[95,777,778],{"class":116}," 'EOF'\n",[95,780,781],{"class":97,"line":554},[95,782,783],{"class":116},"#!/bin/sh\n",[95,785,786],{"class":97,"line":560},[95,787,788],{"class":116},"gitleaks protect --staged --verbose\n",[95,790,791],{"class":97,"line":566},[95,792,793],{"class":116},"if [ $? -ne 0 ]; then\n",[95,795,796],{"class":97,"line":572},[95,797,798],{"class":116},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[95,800,802],{"class":97,"line":801},13,[95,803,804],{"class":116},"  exit 1\n",[95,806,808],{"class":97,"line":807},14,[95,809,810],{"class":116},"fi\n",[95,812,814],{"class":97,"line":813},15,[95,815,816],{"class":116},"EOF\n",[95,818,820,823,826],{"class":97,"line":819},16,[95,821,822],{"class":108},"chmod",[95,824,825],{"class":116}," +x",[95,827,828],{"class":116}," .git/hooks/pre-commit\n",[463,830,832],{"id":831},"railway-build-argument-warning","Railway Build Argument Warning",[10,834,835,836,839],{},"If you are using Docker on Railway and passing secrets as ",[14,837,838],{},"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.",[86,841,845],{"className":842,"code":843,"language":844,"meta":91,"style":91},"language-dockerfile shiki shiki-themes github-light github-dark","# Wrong: secret visible in image layers\nARG OPENAI_API_KEY\nENV OPENAI_API_KEY=$OPENAI_API_KEY\n\n# Right: access process.env.OPENAI_API_KEY at runtime\n# No ARG/ENV for secrets in Dockerfile\n","dockerfile",[14,846,847,852,857,862,866,871],{"__ignoreMap":91},[95,848,849],{"class":97,"line":98},[95,850,851],{},"# Wrong: secret visible in image layers\n",[95,853,854],{"class":97,"line":105},[95,855,856],{},"ARG OPENAI_API_KEY\n",[95,858,859],{"class":97,"line":144},[95,860,861],{},"ENV OPENAI_API_KEY=$OPENAI_API_KEY\n",[95,863,864],{"class":97,"line":343},[95,865,340],{"emptyLinePlaceholder":339},[95,867,868],{"class":97,"line":349},[95,869,870],{},"# Right: access process.env.OPENAI_API_KEY at runtime\n",[95,872,873],{"class":97,"line":366},[95,874,875],{},"# No ARG/ENV for secrets in Dockerfile\n",[877,878,879,890,896,902,908],"faq-section",{},[880,881,883],"faq-item",{"question":882},"How do I know if my Railway API key is exposed?",[10,884,885,886,889],{},"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 ",[14,887,888],{},"git log --all --full-history -- .env"," to see if your .env file was ever committed to git history.",[880,891,893],{"question":892},"What's the difference between Railway Variables and .env files?",[10,894,895],{},"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.",[880,897,899],{"question":898},"Why does VITE_OPENAI_API_KEY expose my key even though it's in Railway?",[10,900,901],{},"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.",[880,903,905],{"question":904},"Do I need to rotate my key if I'm not sure it was accessed?",[10,906,907],{},"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.",[880,909,911],{"question":910},"Can Railway Private Networking protect my database URL?",[10,912,913],{},"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.",[915,916,917,923,928],"related-articles",{},[918,919],"related-card",{"description":920,"href":921,"title":922},"How to add, manage, and secure env vars in Railway: shared variables, references, secrets, and the dashboard walkthrough.","/blog/how-to/railway-env-vars","Railway Environment Variables Guide",[918,924],{"description":925,"href":926,"title":927},"Emergency guide for rotating compromised API keys without downtime. Step-by-step for Stripe, OpenAI, Supabase, and others.","/blog/how-to/rotate-api-keys","How to Rotate API Keys",[918,929],{"description":930,"href":931,"title":932},"Clean secrets from your git history after accidental commits using git filter-repo and BFG Repo Cleaner.","/blog/how-to/remove-secrets-git-history","How to Remove Secrets from Git History",[934,935,938],"cta-box",{"href":936,"label":937},"/","Scan Your Railway App",[10,939,940],{},"Check your Railway deployment for API keys in your JavaScript bundle, exposed environment variables, and security header gaps. Free scan, no signup required.",[942,943,944],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":91,"searchDepth":105,"depth":105,"links":946},[947,948,949,950,954,955],{"id":45,"depth":105,"text":46},{"id":251,"depth":105,"text":252},{"id":306,"depth":105,"text":307},{"id":457,"depth":105,"text":458,"children":951},[952,953],{"id":465,"depth":144,"text":466},{"id":585,"depth":144,"text":586},{"id":654,"depth":105,"text":655},{"id":699,"depth":105,"text":700,"children":956},[957,958],{"id":703,"depth":144,"text":704},{"id":831,"depth":144,"text":832},"how-to","2026-05-24","Railway API key exposure usually comes from VITE_/NEXT_PUBLIC_ prefixes baking secrets into your client bundle. Step-by-step fix: audit, rotate, move to server-side env vars.",false,"md",[965,967,968,970,972],{"question":882,"answer":966},"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's in your client bundle and visible to anyone. Also check: does your .env file exist in your git repository? Run git log --all --full-history -- .env to see if it was ever committed.",{"question":892,"answer":895},{"question":898,"answer":969},"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 prefix) and access it only in your server-side code.",{"question":904,"answer":971},"Yes. Rotate it anyway. It takes under 5 minutes and the cost of not rotating a compromised key is far higher than the cost of updating a valid one. Check the service's usage dashboard for spikes after the potential exposure window.",{"question":910,"answer":973},"Yes. When all your services are in the same Railway project, you can 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. Set DATABASE_URL to the private address and remove the public URL from your variables.","yellow",null,"railway api key exposure, fix railway api key, railway secret leaked, railway environment variable security, railway vite secret, railway next public key",{},"Railway API key exposure fix: find leaked secrets in your JS bundle, rotate them, and move them into Railway's Variables tab so they stay server-side.","/blog/how-to/fix-railway-api-key-exposure","8 min read","[object Object]","HowTo",{"title":5,"description":961},{"loc":979},"blog/how-to/fix-railway-api-key-exposure",[],"summary_large_image","DKBVtHjZCXbq_4bqVKR9reEAv-SpOfQsIocYqvqAKXw",1779752890345]