[{"data":1,"prerenderedAt":1650},["ShallowReactive",2],{"blog-how-to/fix-vercel-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1618,"date":1619,"dateModified":1619,"description":1620,"draft":1621,"extension":1622,"faq":1623,"featured":1621,"headerVariant":1635,"image":1636,"keywords":1637,"meta":1638,"navigation":153,"ogDescription":1639,"ogTitle":1636,"path":1640,"readTime":1641,"schemaOrg":1642,"schemaType":1643,"seo":1644,"sitemap":1645,"stem":1646,"tags":1647,"twitterCard":1648,"__hash__":1649},"blog/blog/how-to/fix-vercel-api-key-exposure.md","How to Fix Vercel API Key Exposure (2026)",{"type":7,"value":8,"toc":1602},"minimark",[9,22,25,44,49,52,83,201,286,299,305,309,312,320,328,336,344,353,359,363,366,440,443,449,516,520,523,528,538,543,625,630,896,903,907,910,962,972,976,990,1086,1090,1097,1253,1256,1262,1266,1273,1276,1292,1295,1303,1306,1310,1314,1431,1435,1529,1571,1590,1598],[10,11,12,13,17,18,21],"p",{},"Vercel's CI pipeline clones your git repository and runs your build. If a secret is in that repo, it ends up in the build logs. If it has a ",[14,15,16],"code",{},"NEXT_PUBLIC_"," prefix, it ends up in every visitor's browser. In our scans of Next.js apps deployed to Vercel, exposed API keys are the most common critical finding, and the majority trace back to one of two mistakes: a misnamed environment variable or a committed ",[14,19,20],{},".env"," file.",[10,23,24],{},"This guide covers both root causes, plus the Vercel-specific risks that other platforms don't have.",[26,27,28],"tldr",{},[10,29,30,31,33,34,36,37,39,40,43],{},"Vercel API key exposure has two main causes: (1) secrets named with ",[14,32,16],{}," baked into the client bundle at build time, and (2) secrets committed to git and pulled into Vercel's CI build logs. Rotate the exposed key immediately. Then either remove the ",[14,35,16],{}," prefix and move the call into a Next.js Route Handler, or clean the secret from git history and add ",[14,38,20],{}," to ",[14,41,42],{},".gitignore",". Preview deploys add a third risk: enable Deployment Protection so every branch push doesn't create a public exposure URL.",[45,46,48],"h2",{"id":47},"step-1-find-the-exposed-key","Step 1: Find the Exposed Key",[10,50,51],{},"Start by confirming exactly what is out and how it got there.",[53,54,56],"step",{"number":55},"1",[10,57,58,62,63,66,67,70,71,74,75,78,79,82],{},[59,60,61],"strong",{},"Check your JS bundle in the browser."," Open your deployed Vercel app, press F12, go to Sources, and search for your key value or its known prefix: ",[14,64,65],{},"sk_proj_"," (OpenAI), ",[14,68,69],{},"sk_live_"," (Stripe), ",[14,72,73],{},"AKIA"," (AWS), or ",[14,76,77],{},"eyJhbGci"," (Supabase JWTs). If it appears inside a ",[14,80,81],{},".js"," file on your domain, it's in your client bundle and visible to anyone.",[53,84,86,91,186],{"number":85},"2",[10,87,88],{},[59,89,90],{},"Search your codebase for NEXT_PUBLIC_-prefixed secrets.",[92,93,98],"pre",{"className":94,"code":95,"language":96,"meta":97,"style":97},"language-bash shiki shiki-themes github-light github-dark","# Search for variables with the public prefix\ngrep -r \"NEXT_PUBLIC_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\n\n# Also check for hardcoded key values directly in component files\ngrep -rE \"(sk_proj_|sk_live_|AKIA|Bearer ey)\" . --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\n","bash","",[14,99,100,109,148,155,161],{"__ignoreMap":97},[101,102,105],"span",{"class":103,"line":104},"line",1,[101,106,108],{"class":107},"sJ8bj","# Search for variables with the public prefix\n",[101,110,112,116,120,124,127,130,133,135,138,140,143,145],{"class":103,"line":111},2,[101,113,115],{"class":114},"sScJk","grep",[101,117,119],{"class":118},"sj4cs"," -r",[101,121,123],{"class":122},"sZZnC"," \"NEXT_PUBLIC_\"",[101,125,126],{"class":122}," .",[101,128,129],{"class":118}," --include=",[101,131,132],{"class":122},"\"*.env*\"",[101,134,129],{"class":118},[101,136,137],{"class":122},"\"*.ts\"",[101,139,129],{"class":118},[101,141,142],{"class":122},"\"*.tsx\"",[101,144,129],{"class":118},[101,146,147],{"class":122},"\"*.js\"\n",[101,149,151],{"class":103,"line":150},3,[101,152,154],{"emptyLinePlaceholder":153},true,"\n",[101,156,158],{"class":103,"line":157},4,[101,159,160],{"class":107},"# Also check for hardcoded key values directly in component files\n",[101,162,164,166,169,172,174,176,178,180,182,184],{"class":103,"line":163},5,[101,165,115],{"class":114},[101,167,168],{"class":118}," -rE",[101,170,171],{"class":122}," \"(sk_proj_|sk_live_|AKIA|Bearer ey)\"",[101,173,126],{"class":122},[101,175,129],{"class":118},[101,177,137],{"class":122},[101,179,129],{"class":118},[101,181,142],{"class":122},[101,183,129],{"class":118},[101,185,147],{"class":122},[10,187,188,189,192,193,196,197,200],{},"Any variable named ",[14,190,191],{},"NEXT_PUBLIC_OPENAI_API_KEY",", ",[14,194,195],{},"NEXT_PUBLIC_STRIPE_SECRET_KEY",", or ",[14,198,199],{},"NEXT_PUBLIC_ANTHROPIC_API_KEY"," is in your bundle. The prefix makes that intentional on Vercel's part. It's your code that's wrong, not Vercel.",[53,202,204,213,283],{"number":203},"3",[10,205,206,209,210,212],{},[59,207,208],{},"Check git history for committed .env files."," Vercel's CI clones your repository to build. If a ",[14,211,20],{}," file was committed at any point, the build logs captured it.",[92,214,216],{"className":94,"code":215,"language":96,"meta":97,"style":97},"git log --all --full-history -- .env\ngit log --all --full-history -- .env.local\ngit log --all --full-history -- .env.production\ngit log --all --full-history -- .env.production.local\n",[14,217,218,238,253,268],{"__ignoreMap":97},[101,219,220,223,226,229,232,235],{"class":103,"line":104},[101,221,222],{"class":114},"git",[101,224,225],{"class":122}," log",[101,227,228],{"class":118}," --all",[101,230,231],{"class":118}," --full-history",[101,233,234],{"class":118}," --",[101,236,237],{"class":122}," .env\n",[101,239,240,242,244,246,248,250],{"class":103,"line":111},[101,241,222],{"class":114},[101,243,225],{"class":122},[101,245,228],{"class":118},[101,247,231],{"class":118},[101,249,234],{"class":118},[101,251,252],{"class":122}," .env.local\n",[101,254,255,257,259,261,263,265],{"class":103,"line":150},[101,256,222],{"class":114},[101,258,225],{"class":122},[101,260,228],{"class":118},[101,262,231],{"class":118},[101,264,234],{"class":118},[101,266,267],{"class":122}," .env.production\n",[101,269,270,272,274,276,278,280],{"class":103,"line":157},[101,271,222],{"class":114},[101,273,225],{"class":122},[101,275,228],{"class":118},[101,277,231],{"class":118},[101,279,234],{"class":118},[101,281,282],{"class":122}," .env.production.local\n",[10,284,285],{},"If these return commits, the secrets lived in your repo history. Anyone who cloned, forked, or starred the repo may have a copy.",[53,287,289],{"number":288},"4",[10,290,291,294,295,298],{},[59,292,293],{},"Check Vercel build logs."," In your Vercel dashboard, go to Deployments and click on a recent build. Search the build output for your key prefix. If build steps print environment variables (a common ",[14,296,297],{},"console.log"," debugging habit), the values are stored in Vercel's log retention for 30 days.",[300,301,302],"tip-box",{},[10,303,304],{},"CheckYourVibe scans your deployed Vercel app and flags API keys found in the JavaScript bundle, including OpenAI, Stripe, Supabase service-role, and AWS credentials. Run a scan before rotating so you know exactly which keys are exposed.",[45,306,308],{"id":307},"step-2-rotate-the-exposed-key-immediately","Step 2: Rotate the Exposed Key Immediately",[10,310,311],{},"Rotate before you fix anything else. The key is already out.",[53,313,314],{"number":55},[10,315,316,319],{},[59,317,318],{},"Generate a new key"," in the affected service dashboard. Most services (OpenAI, Stripe, Supabase) let you create a new key while the old one is still active, so production stays live during the transition.",[53,321,322],{"number":85},[10,323,324,327],{},[59,325,326],{},"Update Vercel environment variables."," In your Vercel dashboard, go to Project Settings > Environment Variables. Find the variable, click the three-dot menu, and update the value. Select which environments (Production, Preview, Development) need the new value.",[53,329,330],{"number":203},[10,331,332,335],{},[59,333,334],{},"Trigger a new deployment."," Vercel doesn't automatically redeploy after an environment variable change. Go to Deployments, find your latest deployment, and click Redeploy.",[53,337,338],{"number":288},[10,339,340,343],{},[59,341,342],{},"Revoke the old key."," Once the new deployment is live, delete or disable the compromised key in the service dashboard. Do not leave it active.",[53,345,347],{"number":346},"5",[10,348,349,352],{},[59,350,351],{},"Review usage logs."," Check the affected service's usage dashboard for spikes during the exposure window. OpenAI shows per-key usage under API keys. Stripe logs show which endpoints were called. Supabase has an API logs view.",[354,355,356],"danger-box",{},[10,357,358],{},"Do not wait to confirm abuse before revoking. Automated scanners index GitHub and deployed apps in real time. Exposed OpenAI keys have been drained within minutes of appearing in public repositories.",[45,360,362],{"id":361},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[10,364,365],{},"If your audit found secrets in git history, scrubbing the current file is not enough. The secret exists in every previous commit.",[92,367,369],{"className":94,"code":368,"language":96,"meta":97,"style":97},"# Install git-filter-repo\npip install git-filter-repo\n\n# Remove all .env variants from history\ngit filter-repo --path .env --invert-paths\ngit filter-repo --path .env.local --invert-paths\ngit filter-repo --path .env.production --invert-paths\n",[14,370,371,376,387,391,396,412,426],{"__ignoreMap":97},[101,372,373],{"class":103,"line":104},[101,374,375],{"class":107},"# Install git-filter-repo\n",[101,377,378,381,384],{"class":103,"line":111},[101,379,380],{"class":114},"pip",[101,382,383],{"class":122}," install",[101,385,386],{"class":122}," git-filter-repo\n",[101,388,389],{"class":103,"line":150},[101,390,154],{"emptyLinePlaceholder":153},[101,392,393],{"class":103,"line":157},[101,394,395],{"class":107},"# Remove all .env variants from history\n",[101,397,398,400,403,406,409],{"class":103,"line":163},[101,399,222],{"class":114},[101,401,402],{"class":122}," filter-repo",[101,404,405],{"class":118}," --path",[101,407,408],{"class":122}," .env",[101,410,411],{"class":118}," --invert-paths\n",[101,413,415,417,419,421,424],{"class":103,"line":414},6,[101,416,222],{"class":114},[101,418,402],{"class":122},[101,420,405],{"class":118},[101,422,423],{"class":122}," .env.local",[101,425,411],{"class":118},[101,427,429,431,433,435,438],{"class":103,"line":428},7,[101,430,222],{"class":114},[101,432,402],{"class":122},[101,434,405],{"class":118},[101,436,437],{"class":122}," .env.production",[101,439,411],{"class":118},[10,441,442],{},"Force-push to all remotes after scrubbing and ask any collaborators to re-clone. If the repo was public at any point during the exposure, treat the key as compromised regardless of whether you can confirm access.",[10,444,445,446,448],{},"Add the env files to ",[14,447,42],{}," before your next commit:",[92,450,452],{"className":94,"code":451,"language":96,"meta":97,"style":97},"echo \".env\" >> .gitignore\necho \".env*.local\" >> .gitignore\necho \".env.production\" >> .gitignore\ngit add .gitignore && git commit -m \"chore: gitignore .env files\"\n",[14,453,454,469,480,491],{"__ignoreMap":97},[101,455,456,459,462,466],{"class":103,"line":104},[101,457,458],{"class":118},"echo",[101,460,461],{"class":122}," \".env\"",[101,463,465],{"class":464},"szBVR"," >>",[101,467,468],{"class":122}," .gitignore\n",[101,470,471,473,476,478],{"class":103,"line":111},[101,472,458],{"class":118},[101,474,475],{"class":122}," \".env*.local\"",[101,477,465],{"class":464},[101,479,468],{"class":122},[101,481,482,484,487,489],{"class":103,"line":150},[101,483,458],{"class":118},[101,485,486],{"class":122}," \".env.production\"",[101,488,465],{"class":464},[101,490,468],{"class":122},[101,492,493,495,498,501,505,507,510,513],{"class":103,"line":157},[101,494,222],{"class":114},[101,496,497],{"class":122}," add",[101,499,500],{"class":122}," .gitignore",[101,502,504],{"class":503},"sVt8B"," && ",[101,506,222],{"class":114},[101,508,509],{"class":122}," commit",[101,511,512],{"class":118}," -m",[101,514,515],{"class":122}," \"chore: gitignore .env files\"\n",[45,517,519],{"id":518},"step-4-fix-the-root-cause-next_public_-prefix","Step 4: Fix the Root Cause (NEXT_PUBLIC_ Prefix)",[10,521,522],{},"Rotating stops the immediate bleeding. This step stops it from happening again.",[524,525,527],"h3",{"id":526},"why-next_public_-exposes-your-key","Why NEXT_PUBLIC_ Exposes Your Key",[10,529,530,531,534,535,537],{},"Next.js statically replaces ",[14,532,533],{},"process.env.NEXT_PUBLIC_*"," references at build time. The value gets embedded literally in the JavaScript output. Even if Vercel stores the variable in encrypted storage, the ",[14,536,16],{}," prefix instructs Next.js to copy it into the bundle.",[10,539,540],{},[59,541,542],{},"Wrong (key ends up in every visitor's browser):",[92,544,548],{"className":545,"code":546,"language":547,"meta":97,"style":97},"language-typescript shiki shiki-themes github-light github-dark","// pages/index.tsx or app/page.tsx (runs in the browser)\nconst response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n  headers: {\n    Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`, // visible to anyone\n  },\n});\n","typescript",[14,549,550,555,581,586,615,620],{"__ignoreMap":97},[101,551,552],{"class":103,"line":104},[101,553,554],{"class":107},"// pages/index.tsx or app/page.tsx (runs in the browser)\n",[101,556,557,560,563,566,569,572,575,578],{"class":103,"line":111},[101,558,559],{"class":464},"const",[101,561,562],{"class":118}," response",[101,564,565],{"class":464}," =",[101,567,568],{"class":464}," await",[101,570,571],{"class":114}," fetch",[101,573,574],{"class":503},"(",[101,576,577],{"class":122},"\"https://api.openai.com/v1/chat/completions\"",[101,579,580],{"class":503},", {\n",[101,582,583],{"class":103,"line":150},[101,584,585],{"class":503},"  headers: {\n",[101,587,588,591,594,597,600,603,605,607,610,612],{"class":103,"line":157},[101,589,590],{"class":503},"    Authorization: ",[101,592,593],{"class":122},"`Bearer ${",[101,595,596],{"class":503},"process",[101,598,599],{"class":122},".",[101,601,602],{"class":503},"env",[101,604,599],{"class":122},[101,606,191],{"class":118},[101,608,609],{"class":122},"}`",[101,611,192],{"class":503},[101,613,614],{"class":107},"// visible to anyone\n",[101,616,617],{"class":103,"line":163},[101,618,619],{"class":503},"  },\n",[101,621,622],{"class":103,"line":414},[101,623,624],{"class":503},"});\n",[10,626,627],{},[59,628,629],{},"Right (key stays server-side via a Route Handler):",[92,631,633],{"className":545,"code":632,"language":547,"meta":97,"style":97},"// app/api/chat/route.ts (server-side only)\nimport { NextRequest, NextResponse } from \"next/server\";\n\nexport async function POST(req: NextRequest) {\n  const { message } = await req.json();\n  const response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // no NEXT_PUBLIC_ prefix\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      model: \"gpt-4o\",\n      messages: [{ role: \"user\", content: message }],\n    }),\n  });\n  const data = await response.json();\n  return NextResponse.json({ reply: data.choices[0].message.content });\n}\n",[14,634,635,640,657,661,690,718,736,747,753,779,793,799,816,827,839,845,851,870,890],{"__ignoreMap":97},[101,636,637],{"class":103,"line":104},[101,638,639],{"class":107},"// app/api/chat/route.ts (server-side only)\n",[101,641,642,645,648,651,654],{"class":103,"line":111},[101,643,644],{"class":464},"import",[101,646,647],{"class":503}," { NextRequest, NextResponse } ",[101,649,650],{"class":464},"from",[101,652,653],{"class":122}," \"next/server\"",[101,655,656],{"class":503},";\n",[101,658,659],{"class":103,"line":150},[101,660,154],{"emptyLinePlaceholder":153},[101,662,663,666,669,672,675,677,681,684,687],{"class":103,"line":157},[101,664,665],{"class":464},"export",[101,667,668],{"class":464}," async",[101,670,671],{"class":464}," function",[101,673,674],{"class":114}," POST",[101,676,574],{"class":503},[101,678,680],{"class":679},"s4XuR","req",[101,682,683],{"class":464},":",[101,685,686],{"class":114}," NextRequest",[101,688,689],{"class":503},") {\n",[101,691,692,695,698,701,704,707,709,712,715],{"class":103,"line":163},[101,693,694],{"class":464},"  const",[101,696,697],{"class":503}," { ",[101,699,700],{"class":118},"message",[101,702,703],{"class":503}," } ",[101,705,706],{"class":464},"=",[101,708,568],{"class":464},[101,710,711],{"class":503}," req.",[101,713,714],{"class":114},"json",[101,716,717],{"class":503},"();\n",[101,719,720,722,724,726,728,730,732,734],{"class":103,"line":414},[101,721,694],{"class":464},[101,723,562],{"class":118},[101,725,565],{"class":464},[101,727,568],{"class":464},[101,729,571],{"class":114},[101,731,574],{"class":503},[101,733,577],{"class":122},[101,735,580],{"class":503},[101,737,738,741,744],{"class":103,"line":428},[101,739,740],{"class":503},"    method: ",[101,742,743],{"class":122},"\"POST\"",[101,745,746],{"class":503},",\n",[101,748,750],{"class":103,"line":749},8,[101,751,752],{"class":503},"    headers: {\n",[101,754,756,759,761,763,765,767,769,772,774,776],{"class":103,"line":755},9,[101,757,758],{"class":503},"      Authorization: ",[101,760,593],{"class":122},[101,762,596],{"class":503},[101,764,599],{"class":122},[101,766,602],{"class":503},[101,768,599],{"class":122},[101,770,771],{"class":118},"OPENAI_API_KEY",[101,773,609],{"class":122},[101,775,192],{"class":503},[101,777,778],{"class":107},"// no NEXT_PUBLIC_ prefix\n",[101,780,782,785,788,791],{"class":103,"line":781},10,[101,783,784],{"class":122},"      \"Content-Type\"",[101,786,787],{"class":503},": ",[101,789,790],{"class":122},"\"application/json\"",[101,792,746],{"class":503},[101,794,796],{"class":103,"line":795},11,[101,797,798],{"class":503},"    },\n",[101,800,802,805,808,810,813],{"class":103,"line":801},12,[101,803,804],{"class":503},"    body: ",[101,806,807],{"class":118},"JSON",[101,809,599],{"class":503},[101,811,812],{"class":114},"stringify",[101,814,815],{"class":503},"({\n",[101,817,819,822,825],{"class":103,"line":818},13,[101,820,821],{"class":503},"      model: ",[101,823,824],{"class":122},"\"gpt-4o\"",[101,826,746],{"class":503},[101,828,830,833,836],{"class":103,"line":829},14,[101,831,832],{"class":503},"      messages: [{ role: ",[101,834,835],{"class":122},"\"user\"",[101,837,838],{"class":503},", content: message }],\n",[101,840,842],{"class":103,"line":841},15,[101,843,844],{"class":503},"    }),\n",[101,846,848],{"class":103,"line":847},16,[101,849,850],{"class":503},"  });\n",[101,852,854,856,859,861,863,866,868],{"class":103,"line":853},17,[101,855,694],{"class":464},[101,857,858],{"class":118}," data",[101,860,565],{"class":464},[101,862,568],{"class":464},[101,864,865],{"class":503}," response.",[101,867,714],{"class":114},[101,869,717],{"class":503},[101,871,873,876,879,881,884,887],{"class":103,"line":872},18,[101,874,875],{"class":464},"  return",[101,877,878],{"class":503}," NextResponse.",[101,880,714],{"class":114},[101,882,883],{"class":503},"({ reply: data.choices[",[101,885,886],{"class":118},"0",[101,888,889],{"class":503},"].message.content });\n",[101,891,893],{"class":103,"line":892},19,[101,894,895],{"class":503},"}\n",[10,897,898,899,902],{},"Your React component calls ",[14,900,901],{},"/api/chat"," on your own domain. Your Route Handler calls OpenAI. The key never reaches the browser.",[524,904,906],{"id":905},"variable-naming-in-vercel","Variable Naming in Vercel",[10,908,909],{},"In Vercel's environment variables panel, store secrets without any public prefix:",[911,912,913,926],"table",{},[914,915,916],"thead",{},[917,918,919,923],"tr",{},[920,921,922],"th",{},"Wrong",[920,924,925],{},"Right",[927,928,929,940,951],"tbody",{},[917,930,931,936],{},[932,933,934],"td",{},[14,935,191],{},[932,937,938],{},[14,939,771],{},[917,941,942,946],{},[932,943,944],{},[14,945,199],{},[932,947,948],{},[14,949,950],{},"ANTHROPIC_API_KEY",[917,952,953,957],{},[932,954,955],{},[14,956,195],{},[932,958,959],{},[14,960,961],{},"STRIPE_SECRET_KEY",[10,963,964,965,967,968,971],{},"Variables without ",[14,966,16],{}," are available only in server-side code: Route Handlers, Server Actions, ",[14,969,970],{},"getServerSideProps",", and API routes.",[524,973,975],{"id":974},"if-youre-using-vite-on-vercel-not-nextjs","If You're Using Vite on Vercel (not Next.js)",[10,977,978,979,982,983,985,986,989],{},"Vite uses the ",[14,980,981],{},"VITE_"," prefix instead. The same rule applies: any ",[14,984,981],{},"-prefixed secret ends up in the client bundle. Move the secret call to a Vercel Serverless Function (a file in ",[14,987,988],{},"/api/","):",[92,991,993],{"className":545,"code":992,"language":547,"meta":97,"style":97},"// api/chat.ts (Vercel Serverless Function)\nimport type { VercelRequest, VercelResponse } from \"@vercel/node\";\n\nexport default async function handler(req: VercelRequest, res: VercelResponse) {\n  // process.env.OPENAI_API_KEY is safe here, no VITE_ prefix needed\n  const apiKey = process.env.OPENAI_API_KEY;\n  // ... call OpenAI, return result\n}\n",[14,994,995,1000,1017,1021,1056,1061,1077,1082],{"__ignoreMap":97},[101,996,997],{"class":103,"line":104},[101,998,999],{"class":107},"// api/chat.ts (Vercel Serverless Function)\n",[101,1001,1002,1004,1007,1010,1012,1015],{"class":103,"line":111},[101,1003,644],{"class":464},[101,1005,1006],{"class":464}," type",[101,1008,1009],{"class":503}," { VercelRequest, VercelResponse } ",[101,1011,650],{"class":464},[101,1013,1014],{"class":122}," \"@vercel/node\"",[101,1016,656],{"class":503},[101,1018,1019],{"class":103,"line":150},[101,1020,154],{"emptyLinePlaceholder":153},[101,1022,1023,1025,1028,1030,1032,1035,1037,1039,1041,1044,1046,1049,1051,1054],{"class":103,"line":157},[101,1024,665],{"class":464},[101,1026,1027],{"class":464}," default",[101,1029,668],{"class":464},[101,1031,671],{"class":464},[101,1033,1034],{"class":114}," handler",[101,1036,574],{"class":503},[101,1038,680],{"class":679},[101,1040,683],{"class":464},[101,1042,1043],{"class":114}," VercelRequest",[101,1045,192],{"class":503},[101,1047,1048],{"class":679},"res",[101,1050,683],{"class":464},[101,1052,1053],{"class":114}," VercelResponse",[101,1055,689],{"class":503},[101,1057,1058],{"class":103,"line":163},[101,1059,1060],{"class":107},"  // process.env.OPENAI_API_KEY is safe here, no VITE_ prefix needed\n",[101,1062,1063,1065,1068,1070,1073,1075],{"class":103,"line":414},[101,1064,694],{"class":464},[101,1066,1067],{"class":118}," apiKey",[101,1069,565],{"class":464},[101,1071,1072],{"class":503}," process.env.",[101,1074,771],{"class":118},[101,1076,656],{"class":503},[101,1078,1079],{"class":103,"line":428},[101,1080,1081],{"class":107},"  // ... call OpenAI, return result\n",[101,1083,1084],{"class":103,"line":749},[101,1085,895],{"class":503},[45,1087,1089],{"id":1088},"step-5-fix-supabase-service_role-misuse","Step 5: Fix Supabase service_role Misuse",[10,1091,1092,1093,1096],{},"AI tools sometimes generate Supabase clients in frontend code using the ",[14,1094,1095],{},"service_role"," key. That key bypasses every Row Level Security policy in your database.",[92,1098,1100],{"className":545,"code":1099,"language":547,"meta":97,"style":97},"// Wrong: service_role in a React component\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // full DB access to every visitor\n);\n\n// Right: anon key in components, service_role only in Route Handlers\n// Component:\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // safe, controlled by RLS\n);\n\n// app/api/admin/route.ts (server only):\nconst adminClient = createClient(\n  process.env.SUPABASE_URL!,\n  process.env.SUPABASE_SERVICE_ROLE_KEY! // no NEXT_PUBLIC_ prefix\n);\n",[14,1101,1102,1107,1122,1135,1147,1152,1156,1161,1166,1178,1188,1200,1204,1208,1213,1226,1237,1249],{"__ignoreMap":97},[101,1103,1104],{"class":103,"line":104},[101,1105,1106],{"class":107},"// Wrong: service_role in a React component\n",[101,1108,1109,1111,1114,1116,1119],{"class":103,"line":111},[101,1110,559],{"class":464},[101,1112,1113],{"class":118}," supabase",[101,1115,565],{"class":464},[101,1117,1118],{"class":114}," createClient",[101,1120,1121],{"class":503},"(\n",[101,1123,1124,1127,1130,1133],{"class":103,"line":150},[101,1125,1126],{"class":503},"  process.env.",[101,1128,1129],{"class":118},"NEXT_PUBLIC_SUPABASE_URL",[101,1131,1132],{"class":464},"!",[101,1134,746],{"class":503},[101,1136,1137,1139,1142,1144],{"class":103,"line":157},[101,1138,1126],{"class":503},[101,1140,1141],{"class":118},"NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY",[101,1143,1132],{"class":464},[101,1145,1146],{"class":107}," // full DB access to every visitor\n",[101,1148,1149],{"class":103,"line":163},[101,1150,1151],{"class":503},");\n",[101,1153,1154],{"class":103,"line":414},[101,1155,154],{"emptyLinePlaceholder":153},[101,1157,1158],{"class":103,"line":428},[101,1159,1160],{"class":107},"// Right: anon key in components, service_role only in Route Handlers\n",[101,1162,1163],{"class":103,"line":749},[101,1164,1165],{"class":107},"// Component:\n",[101,1167,1168,1170,1172,1174,1176],{"class":103,"line":755},[101,1169,559],{"class":464},[101,1171,1113],{"class":118},[101,1173,565],{"class":464},[101,1175,1118],{"class":114},[101,1177,1121],{"class":503},[101,1179,1180,1182,1184,1186],{"class":103,"line":781},[101,1181,1126],{"class":503},[101,1183,1129],{"class":118},[101,1185,1132],{"class":464},[101,1187,746],{"class":503},[101,1189,1190,1192,1195,1197],{"class":103,"line":795},[101,1191,1126],{"class":503},[101,1193,1194],{"class":118},"NEXT_PUBLIC_SUPABASE_ANON_KEY",[101,1196,1132],{"class":464},[101,1198,1199],{"class":107}," // safe, controlled by RLS\n",[101,1201,1202],{"class":103,"line":801},[101,1203,1151],{"class":503},[101,1205,1206],{"class":103,"line":818},[101,1207,154],{"emptyLinePlaceholder":153},[101,1209,1210],{"class":103,"line":829},[101,1211,1212],{"class":107},"// app/api/admin/route.ts (server only):\n",[101,1214,1215,1217,1220,1222,1224],{"class":103,"line":841},[101,1216,559],{"class":464},[101,1218,1219],{"class":118}," adminClient",[101,1221,565],{"class":464},[101,1223,1118],{"class":114},[101,1225,1121],{"class":503},[101,1227,1228,1230,1233,1235],{"class":103,"line":847},[101,1229,1126],{"class":503},[101,1231,1232],{"class":118},"SUPABASE_URL",[101,1234,1132],{"class":464},[101,1236,746],{"class":503},[101,1238,1239,1241,1244,1246],{"class":103,"line":853},[101,1240,1126],{"class":503},[101,1242,1243],{"class":118},"SUPABASE_SERVICE_ROLE_KEY",[101,1245,1132],{"class":464},[101,1247,1248],{"class":107}," // no NEXT_PUBLIC_ prefix\n",[101,1250,1251],{"class":103,"line":872},[101,1252,1151],{"class":503},[10,1254,1255],{},"Enable RLS on every Supabase table. The anon key is safe to expose precisely because RLS restricts what it can do.",[1257,1258,1259],"warning-box",{},[10,1260,1261],{},"If you rotated your Supabase service_role key, update the Supabase dashboard under Project Settings > API, then update your Vercel environment variable. The old key stops working immediately (including any legitimate server-side admin tasks), so update both before revoking.",[45,1263,1265],{"id":1264},"step-6-lock-down-preview-deploys","Step 6: Lock Down Preview Deploys",[10,1267,1268,1269,1272],{},"Every git branch push to Vercel creates a preview deployment at a public URL like ",[14,1270,1271],{},"your-app-git-branch-yourteam.vercel.app",". By default, that URL is accessible to anyone without authentication, including all your environment variables.",[10,1274,1275],{},"Go to Project Settings > Deployment Protection and enable:",[1277,1278,1279,1286],"ul",{},[1280,1281,1282,1285],"li",{},[59,1283,1284],{},"Vercel Authentication",": requires a Vercel account login to access preview URLs",[1280,1287,1288,1291],{},[59,1289,1290],{},"Password Protection",": prompts for a password on all preview deployments",[10,1293,1294],{},"Set Preview environment variables to dummy values (different from production) so leaked preview builds don't expose production keys:",[92,1296,1301],{"className":1297,"code":1299,"language":1300},[1298],"language-text","OPENAI_API_KEY = sk_preview_dummy_value_that_wont_drain_credits\n","text",[14,1302,1299],{"__ignoreMap":97},[10,1304,1305],{},"This limits blast radius when a preview URL leaks.",[45,1307,1309],{"id":1308},"step-7-prevent-future-leaks","Step 7: Prevent Future Leaks",[524,1311,1313],{"id":1312},"pre-commit-hook-with-gitleaks","Pre-commit Hook with Gitleaks",[92,1315,1317],{"className":94,"code":1316,"language":96,"meta":97,"style":97},"# Install gitleaks (macOS)\nbrew install gitleaks\n\n# Test your current repo\ngitleaks detect --source . --verbose\n\n# Add pre-commit hook\ncat > .git/hooks/pre-commit \u003C\u003C 'HOOKEOF'\n#!/bin/sh\ngitleaks protect --staged --verbose\nif [ $? -ne 0 ]; then\n  echo \"Gitleaks found potential secrets. Commit blocked.\"\n  exit 1\nfi\nHOOKEOF\nchmod +x .git/hooks/pre-commit\n",[14,1318,1319,1324,1334,1338,1343,1359,1363,1368,1385,1390,1395,1400,1405,1410,1415,1420],{"__ignoreMap":97},[101,1320,1321],{"class":103,"line":104},[101,1322,1323],{"class":107},"# Install gitleaks (macOS)\n",[101,1325,1326,1329,1331],{"class":103,"line":111},[101,1327,1328],{"class":114},"brew",[101,1330,383],{"class":122},[101,1332,1333],{"class":122}," gitleaks\n",[101,1335,1336],{"class":103,"line":150},[101,1337,154],{"emptyLinePlaceholder":153},[101,1339,1340],{"class":103,"line":157},[101,1341,1342],{"class":107},"# Test your current repo\n",[101,1344,1345,1348,1351,1354,1356],{"class":103,"line":163},[101,1346,1347],{"class":114},"gitleaks",[101,1349,1350],{"class":122}," detect",[101,1352,1353],{"class":118}," --source",[101,1355,126],{"class":122},[101,1357,1358],{"class":118}," --verbose\n",[101,1360,1361],{"class":103,"line":414},[101,1362,154],{"emptyLinePlaceholder":153},[101,1364,1365],{"class":103,"line":428},[101,1366,1367],{"class":107},"# Add pre-commit hook\n",[101,1369,1370,1373,1376,1379,1382],{"class":103,"line":749},[101,1371,1372],{"class":114},"cat",[101,1374,1375],{"class":464}," >",[101,1377,1378],{"class":122}," .git/hooks/pre-commit",[101,1380,1381],{"class":464}," \u003C\u003C",[101,1383,1384],{"class":122}," 'HOOKEOF'\n",[101,1386,1387],{"class":103,"line":755},[101,1388,1389],{"class":122},"#!/bin/sh\n",[101,1391,1392],{"class":103,"line":781},[101,1393,1394],{"class":122},"gitleaks protect --staged --verbose\n",[101,1396,1397],{"class":103,"line":795},[101,1398,1399],{"class":122},"if [ $? -ne 0 ]; then\n",[101,1401,1402],{"class":103,"line":801},[101,1403,1404],{"class":122},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[101,1406,1407],{"class":103,"line":818},[101,1408,1409],{"class":122},"  exit 1\n",[101,1411,1412],{"class":103,"line":829},[101,1413,1414],{"class":122},"fi\n",[101,1416,1417],{"class":103,"line":841},[101,1418,1419],{"class":122},"HOOKEOF\n",[101,1421,1422,1425,1428],{"class":103,"line":847},[101,1423,1424],{"class":114},"chmod",[101,1426,1427],{"class":122}," +x",[101,1429,1430],{"class":122}," .git/hooks/pre-commit\n",[524,1432,1434],{"id":1433},"where-secrets-go-on-vercel","Where Secrets Go on Vercel",[911,1436,1437,1450],{},[914,1438,1439],{},[917,1440,1441,1444,1447],{},[920,1442,1443],{},"Secret type",[920,1445,1446],{},"Where it lives in Vercel",[920,1448,1449],{},"How to access it",[927,1451,1452,1466,1478,1492,1505,1518],{},[917,1453,1454,1457,1460],{},[932,1455,1456],{},"OpenAI / Anthropic key",[932,1458,1459],{},"Environment Variables (no NEXT_PUBLIC_)",[932,1461,1462,1465],{},[14,1463,1464],{},"process.env.OPENAI_API_KEY"," in a Route Handler",[917,1467,1468,1471,1473],{},[932,1469,1470],{},"Stripe secret key",[932,1472,1459],{},[932,1474,1475,1465],{},[14,1476,1477],{},"process.env.STRIPE_SECRET_KEY",[917,1479,1480,1483,1486],{},[932,1481,1482],{},"Supabase anon key",[932,1484,1485],{},"Environment Variables with NEXT_PUBLIC_",[932,1487,1488,1491],{},[14,1489,1490],{},"process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY"," (public by design)",[917,1493,1494,1497,1499],{},[932,1495,1496],{},"Supabase service_role",[932,1498,1459],{},[932,1500,1501,1504],{},[14,1502,1503],{},"process.env.SUPABASE_SERVICE_ROLE_KEY"," in a Route Handler only",[917,1506,1507,1510,1512],{},[932,1508,1509],{},"Database URL",[932,1511,1459],{},[932,1513,1514,1517],{},[14,1515,1516],{},"process.env.DATABASE_URL"," in server-side code only",[917,1519,1520,1523,1526],{},[932,1521,1522],{},"Vercel token",[932,1524,1525],{},"Never in project env vars",[932,1527,1528],{},"Revoke and regenerate at account level",[1530,1531,1532,1547,1553,1559,1565],"faq-section",{},[1533,1534,1536],"faq-item",{"question":1535},"How do I know if my Vercel app has an exposed API key?",[10,1537,1538,1539,1542,1543,1546],{},"Open your deployed app, press F12, go to Sources, and search for your key's prefix (sk_proj_, sk_live_, AKIA, eyJhbGci). If it's in a .js file on your domain, it's in the bundle. Also run ",[14,1540,1541],{},"grep -r 'NEXT_PUBLIC_' ."," in your codebase and ",[14,1544,1545],{},"git log --all -- .env"," to check git history.",[1533,1548,1550],{"question":1549},"What does NEXT_PUBLIC_ actually do in Next.js?",[10,1551,1552],{},"Next.js bakes any NEXT_PUBLIC_-prefixed variable into your client-side JavaScript at build time. Even when stored in Vercel's encrypted environment variables panel, the prefix tells the build system to copy the value into the output bundle. It's designed to make values available in the browser. Using it with secrets is the wrong tool.",[1533,1554,1556],{"question":1555},"Can Vercel preview deploys expose my secrets?",[10,1557,1558],{},"Yes. By default, preview deployments are publicly accessible URLs generated on every branch push. Any NEXT_PUBLIC_ variables in those builds are visible to anyone who finds the URL. Enable Deployment Protection under Project Settings to require authentication before accessing preview URLs.",[1533,1560,1562],{"question":1561},"Do I need to rotate if I'm not sure the key was accessed?",[10,1563,1564],{},"Yes. Rotate regardless. Automated scanners index public repos and Vercel deployments in real time. An OpenAI key exposed in a public repo can be drained within minutes. Rotation takes under 5 minutes and costs nothing.",[1533,1566,1568],{"question":1567},"My Vercel token ended up in a log. What do I do?",[10,1569,1570],{},"Revoke it immediately at vercel.com/account/tokens. A Vercel token gives full API access to your account, including the ability to read environment variables from every project. It's more severe than a single service API key. After revoking, audit your projects' environment variables in case the token was used to read or modify them.",[1572,1573,1574,1580,1585],"related-articles",{},[1575,1576],"related-card",{"description":1577,"href":1578,"title":1579},"Lock down your Vercel deployment: NEXT_PUBLIC_ leak audit, preview-deploy access control, security headers, and env-var hygiene after the April 2026 incident.","/blog/best-practices/vercel","Vercel Security Best Practices (2026)",[1575,1581],{"description":1582,"href":1583,"title":1584},"Emergency guide for rotating compromised API keys without downtime. Step-by-step for OpenAI, Stripe, Supabase, and others.","/blog/how-to/rotate-api-keys","How to Rotate API Keys",[1575,1586],{"description":1587,"href":1588,"title":1589},"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",[1591,1592,1595],"cta-box",{"href":1593,"label":1594},"/","Scan Your Vercel App",[10,1596,1597],{},"Check your Vercel deployment for API keys in your JavaScript bundle, Supabase service_role misuse, and missing security headers. Free scan, no signup required.",[1599,1600,1601],"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}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":97,"searchDepth":111,"depth":111,"links":1603},[1604,1605,1606,1607,1612,1613,1614],{"id":47,"depth":111,"text":48},{"id":307,"depth":111,"text":308},{"id":361,"depth":111,"text":362},{"id":518,"depth":111,"text":519,"children":1608},[1609,1610,1611],{"id":526,"depth":150,"text":527},{"id":905,"depth":150,"text":906},{"id":974,"depth":150,"text":975},{"id":1088,"depth":111,"text":1089},{"id":1264,"depth":111,"text":1265},{"id":1308,"depth":111,"text":1309,"children":1615},[1616,1617],{"id":1312,"depth":150,"text":1313},{"id":1433,"depth":150,"text":1434},"how-to","2026-06-01","Vercel API key exposure usually means a NEXT_PUBLIC_-prefixed secret baked into your Next.js bundle, or a key committed to git and captured during Vercel's CI build. Step-by-step fix: find, rotate, and move secrets server-side.",false,"md",[1624,1626,1628,1630,1632],{"question":1535,"answer":1625},"Open your deployed app in the browser, press F12, go to Sources, and search for your key value or its prefix (sk_proj_, sk_live_, AKIA, or eyJhbGci for Supabase JWTs). If it appears in a .js file, it's in your client bundle. Also run: grep -r 'NEXT_PUBLIC_' . in your codebase to catch prefix-exposed variables, and check git log --all -- .env to see if secrets were ever committed.",{"question":1549,"answer":1627},"Next.js bakes any variable prefixed with NEXT_PUBLIC_ into your client-side JavaScript at build time. Vercel injects those values during the CI build, but they end up as plain text in every .js file served to visitors. Even if the variable is stored safely in Vercel's encrypted environment variables panel, the NEXT_PUBLIC_ prefix makes it public by design.",{"question":1555,"answer":1629},"Yes. By default, Vercel preview deployments are publicly accessible URLs. Anyone with the URL can load the app and read any NEXT_PUBLIC_-prefixed variables from the JavaScript bundle. Without Deployment Protection enabled, every branch push creates a potential exposure point.",{"question":1561,"answer":1631},"Yes. Rotate regardless. Automated scanners index GitHub and deployed sites in real time. An OpenAI key exposed for 24 hours can generate thousands of dollars in unexpected API usage. Rotation takes under 5 minutes and the cost of not rotating is far higher.",{"question":1633,"answer":1634},"My Vercel VERCEL_TOKEN ended up in a log. What do I do?","Revoke it immediately from your Vercel account under Settings > Tokens. A Vercel token gives full API access to your account, including the ability to read environment variables from other projects. Treat it as higher-severity than a single service API key.","yellow",null,"vercel api key exposure, fix vercel api key, next_public secret exposed, vercel environment variables security, nextjs api route handler, vercel token leaked, vercel preview deploy secrets",{},"Vercel API key exposure fix: find NEXT_PUBLIC_ secrets in your JS bundle, rotate them, and move them to Vercel environment variables or Route Handlers.","/blog/how-to/fix-vercel-api-key-exposure","10 min read","[object Object]","HowTo",{"title":5,"description":1620},{"loc":1640},"blog/how-to/fix-vercel-api-key-exposure",[],"summary_large_image","oo_NkP_0O2yBZjGltiJVyW2oUl5afQRNWYWkoZ4ASq8",1780341510707]