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