[{"data":1,"prerenderedAt":1935},["ShallowReactive",2],{"blog-how-to/fix-v0-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1904,"date":1905,"dateModified":1905,"description":1906,"draft":1907,"extension":1908,"faq":1909,"featured":1907,"headerVariant":1920,"image":1921,"keywords":1922,"meta":1923,"navigation":194,"ogDescription":1924,"ogTitle":1921,"path":1925,"readTime":1926,"schemaOrg":1927,"schemaType":1928,"seo":1929,"sitemap":1930,"stem":1931,"tags":1932,"twitterCard":1933,"__hash__":1934},"blog/blog/how-to/fix-v0-api-key-exposure.md","How to Fix v0 API Key Exposure (2026)",{"type":7,"value":8,"toc":1888},"minimark",[9,22,25,41,46,49,63,69,73,76,106,233,291,357,363,367,370,378,386,394,402,411,417,421,424,495,498,509,586,590,593,598,604,609,824,829,1005,1148,1155,1159,1166,1293,1297,1300,1352,1359,1363,1369,1372,1377,1456,1461,1594,1597,1603,1607,1611,1614,1731,1735,1819,1857,1876,1884],[10,11,12,13,17,18,21],"p",{},"A v0-generated Next.js app with an exposed OpenAI key can drain your credits in hours. In our scans of Next.js projects deployed on Vercel, the single most common secret exposure pattern is a ",[14,15,16],"code",{},"NEXT_PUBLIC_OPENAI_API_KEY"," variable that Next.js silently baked into the client JavaScript bundle at build time. The ",[14,19,20],{},"NEXT_PUBLIC_"," prefix is Next.js's way of making values available to browser code, and it works exactly as advertised, including for secrets you did not intend to share.",[10,23,24],{},"This guide walks you through finding the leak, rotating the key, and fixing the root cause so it does not happen again.",[26,27,28],"tldr",{},[10,29,30,31,33,34,37,38,40],{},"v0 API key exposure has two main causes: (1) secrets with a ",[14,32,20],{}," prefix baked into the client bundle by Next.js, and (2) v0 generating code that uses your Supabase ",[14,35,36],{},"service_role"," key in a component or client-side hook. Rotate the exposed key immediately, then either remove the ",[14,39,20],{}," prefix and move the call to a Route Handler or Server Action, or switch from service_role to the anon key with RLS policies. Do not delay rotation waiting to confirm abuse.",[42,43,45],"h2",{"id":44},"why-v0-apps-have-this-problem","Why v0 Apps Have This Problem",[10,47,48],{},"v0 started as a UI component generator, but it has grown into a full Next.js app builder. When you ask it to add AI features, payment processing, or data fetching, it generates code that calls third-party APIs. That generated code often uses environment variables with the wrong prefix.",[10,50,51,58,59,62],{},[52,53,54,55,57],"strong",{},"v0 generates ",[14,56,20],{}," variables because they work."," When v0 adds an OpenAI integration, it may produce ",[14,60,61],{},"process.env.NEXT_PUBLIC_OPENAI_API_KEY"," because this makes the variable accessible to the React component. The code runs correctly in development. In production, that key is also accessible to every person who visits your site.",[10,64,65,68],{},[52,66,67],{},"v0 targets Vercel deployments."," Unlike Bolt.new (which targets Netlify) or Replit (which runs its own servers), v0 apps are built for Vercel and use the Next.js App Router or Pages Router. The fix involves Vercel environment variables and Next.js Route Handlers or Server Actions, not Netlify Functions or Replit Secrets.",[42,70,72],{"id":71},"step-1-find-the-exposed-key","Step 1: Find the Exposed Key",[10,74,75],{},"Before rotating, confirm exactly what is exposed and how.",[77,78,80],"step",{"number":79},"1",[10,81,82,85,86,89,90,93,94,97,98,101,102,105],{},[52,83,84],{},"Check your JS bundle in the browser."," Open your deployed v0 app, press F12, go to Sources, and search for your key value or its known prefix: ",[14,87,88],{},"sk_proj_"," (OpenAI), ",[14,91,92],{},"sk_live_"," (Stripe), ",[14,95,96],{},"AKIA"," (AWS), or ",[14,99,100],{},"eyJhbGci"," (Supabase JWTs). If it appears inside a ",[14,103,104],{},".js"," file served from your domain, it is in your client bundle and visible to any visitor.",[77,107,109,114,219],{"number":108},"2",[10,110,111],{},[52,112,113],{},"Search your codebase for NEXT_PUBLIC_-prefixed secrets.",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-bash shiki shiki-themes github-light github-dark","# In your v0 project root\ngrep -r \"NEXT_PUBLIC_\" . \\\n  --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" --include=\"*.env*\" \\\n  --exclude-dir=node_modules --exclude-dir=.next\n\n# Also check .env.local for the prefix\ngrep \"NEXT_PUBLIC_\" .env.local 2>/dev/null\n","bash","",[14,122,123,132,153,180,189,196,202],{"__ignoreMap":120},[124,125,128],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"sJ8bj","# In your v0 project root\n",[124,133,135,139,143,147,150],{"class":126,"line":134},2,[124,136,138],{"class":137},"sScJk","grep",[124,140,142],{"class":141},"sj4cs"," -r",[124,144,146],{"class":145},"sZZnC"," \"NEXT_PUBLIC_\"",[124,148,149],{"class":145}," .",[124,151,152],{"class":141}," \\\n",[124,154,156,159,162,165,168,170,173,175,178],{"class":126,"line":155},3,[124,157,158],{"class":141},"  --include=",[124,160,161],{"class":145},"\"*.ts\"",[124,163,164],{"class":141}," --include=",[124,166,167],{"class":145},"\"*.tsx\"",[124,169,164],{"class":141},[124,171,172],{"class":145},"\"*.js\"",[124,174,164],{"class":141},[124,176,177],{"class":145},"\"*.env*\"",[124,179,152],{"class":141},[124,181,183,186],{"class":126,"line":182},4,[124,184,185],{"class":141},"  --exclude-dir=node_modules",[124,187,188],{"class":141}," --exclude-dir=.next\n",[124,190,192],{"class":126,"line":191},5,[124,193,195],{"emptyLinePlaceholder":194},true,"\n",[124,197,199],{"class":126,"line":198},6,[124,200,201],{"class":130},"# Also check .env.local for the prefix\n",[124,203,205,207,209,212,216],{"class":126,"line":204},7,[124,206,138],{"class":137},[124,208,146],{"class":145},[124,210,211],{"class":145}," .env.local",[124,213,215],{"class":214},"szBVR"," 2>",[124,217,218],{"class":145},"/dev/null\n",[10,220,221,222,224,225,228,229,232],{},"Any variable named ",[14,223,16],{},", ",[14,226,227],{},"NEXT_PUBLIC_STRIPE_SECRET_KEY",", or ",[14,230,231],{},"NEXT_PUBLIC_ANTHROPIC_API_KEY"," is almost certainly in your bundle. Rename it (drop the prefix) and move the API call server-side.",[77,234,236,245,284],{"number":235},"3",[10,237,238,241,242,244],{},[52,239,240],{},"Check for hardcoded Supabase service_role usage."," v0 sometimes generates a Supabase client in a component using the ",[14,243,36],{}," key:",[115,246,248],{"className":117,"code":247,"language":119,"meta":120,"style":120},"grep -r \"service_role\\|SERVICE_ROLE\" . \\\n  --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" \\\n  --exclude-dir=node_modules\n",[14,249,250,263,279],{"__ignoreMap":120},[124,251,252,254,256,259,261],{"class":126,"line":127},[124,253,138],{"class":137},[124,255,142],{"class":141},[124,257,258],{"class":145}," \"service_role\\|SERVICE_ROLE\"",[124,260,149],{"class":145},[124,262,152],{"class":141},[124,264,265,267,269,271,273,275,277],{"class":126,"line":134},[124,266,158],{"class":141},[124,268,161],{"class":145},[124,270,164],{"class":141},[124,272,167],{"class":145},[124,274,164],{"class":141},[124,276,172],{"class":145},[124,278,152],{"class":141},[124,280,281],{"class":126,"line":155},[124,282,283],{"class":141},"  --exclude-dir=node_modules\n",[10,285,286,287,290],{},"The anon key (",[14,288,289],{},"NEXT_PUBLIC_SUPABASE_ANON_KEY",") is designed to be public. The service_role key is not: it bypasses all Row Level Security.",[77,292,294,299,354],{"number":293},"4",[10,295,296],{},[52,297,298],{},"Check git history for committed .env files.",[115,300,302],{"className":117,"code":301,"language":119,"meta":120,"style":120},"git log --all --full-history -- .env.local\ngit log --all --full-history -- .env.production\ngit log --all --full-history -- .env\n",[14,303,304,324,339],{"__ignoreMap":120},[124,305,306,309,312,315,318,321],{"class":126,"line":127},[124,307,308],{"class":137},"git",[124,310,311],{"class":145}," log",[124,313,314],{"class":141}," --all",[124,316,317],{"class":141}," --full-history",[124,319,320],{"class":141}," --",[124,322,323],{"class":145}," .env.local\n",[124,325,326,328,330,332,334,336],{"class":126,"line":134},[124,327,308],{"class":137},[124,329,311],{"class":145},[124,331,314],{"class":141},[124,333,317],{"class":141},[124,335,320],{"class":141},[124,337,338],{"class":145}," .env.production\n",[124,340,341,343,345,347,349,351],{"class":126,"line":155},[124,342,308],{"class":137},[124,344,311],{"class":145},[124,346,314],{"class":141},[124,348,317],{"class":141},[124,350,320],{"class":141},[124,352,353],{"class":145}," .env\n",[10,355,356],{},"If any of these return commits, the secrets were stored in your repo's history. Anyone who cloned or forked the repo has a copy, even if you deleted the file later.",[358,359,360],"tip-box",{},[10,361,362],{},"CheckYourVibe scans your deployed v0 app and flags API keys found in the JavaScript bundle, including OpenAI, Anthropic, Stripe, Supabase service-role, and AWS credentials. It reads what an attacker reads from your public bundle, often before you realize the key is there.",[42,364,366],{"id":365},"step-2-rotate-the-exposed-key-immediately","Step 2: Rotate the Exposed Key Immediately",[10,368,369],{},"Do not try to fix the code first. The key is already out. Rotate it now.",[77,371,372],{"number":79},[10,373,374,377],{},[52,375,376],{},"Generate a new key"," in the affected service's dashboard (OpenAI, Stripe, Supabase, etc.). Most services allow multiple active keys so production stays live during the transition.",[77,379,380],{"number":108},[10,381,382,385],{},[52,383,384],{},"Update Vercel environment variables."," In the Vercel dashboard, go to your Project > Settings > Environment Variables. Find the variable and update its value. Make sure to set it for Production (and optionally Preview and Development). Vercel will use the new value on the next deploy.",[77,387,388],{"number":235},[10,389,390,393],{},[52,391,392],{},"Redeploy."," In Vercel, go to Deployments and trigger a redeploy, or push a commit to your main branch. The new deploy will use the updated secret.",[77,395,396],{"number":293},[10,397,398,401],{},[52,399,400],{},"Revoke the old key"," in the service dashboard. Delete or disable the compromised key. A key left active is still usable by anyone who copied it from your bundle.",[77,403,405],{"number":404},"5",[10,406,407,410],{},[52,408,409],{},"Check usage logs."," Review the affected service's dashboard for spikes during the exposure window. OpenAI shows per-key usage in the API dashboard. If you see unexpected requests, note the timeframe and endpoints.",[412,413,414],"danger-box",{},[10,415,416],{},"Do not delay revocation waiting to confirm abuse. API abuse can cost thousands of dollars within hours of exposure. Rotate first, investigate second.",[42,418,420],{"id":419},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[10,422,423],{},"If your audit found secrets in git history, scrub them from every commit.",[115,425,427],{"className":117,"code":426,"language":119,"meta":120,"style":120},"# Install git-filter-repo\npip install git-filter-repo\n\n# Remove .env.local from all history\ngit filter-repo --path .env.local --invert-paths\ngit filter-repo --path .env.production --invert-paths\ngit filter-repo --path .env --invert-paths\n",[14,428,429,434,445,449,454,469,482],{"__ignoreMap":120},[124,430,431],{"class":126,"line":127},[124,432,433],{"class":130},"# Install git-filter-repo\n",[124,435,436,439,442],{"class":126,"line":134},[124,437,438],{"class":137},"pip",[124,440,441],{"class":145}," install",[124,443,444],{"class":145}," git-filter-repo\n",[124,446,447],{"class":126,"line":155},[124,448,195],{"emptyLinePlaceholder":194},[124,450,451],{"class":126,"line":182},[124,452,453],{"class":130},"# Remove .env.local from all history\n",[124,455,456,458,461,464,466],{"class":126,"line":191},[124,457,308],{"class":137},[124,459,460],{"class":145}," filter-repo",[124,462,463],{"class":141}," --path",[124,465,211],{"class":145},[124,467,468],{"class":141}," --invert-paths\n",[124,470,471,473,475,477,480],{"class":126,"line":198},[124,472,308],{"class":137},[124,474,460],{"class":145},[124,476,463],{"class":141},[124,478,479],{"class":145}," .env.production",[124,481,468],{"class":141},[124,483,484,486,488,490,493],{"class":126,"line":204},[124,485,308],{"class":137},[124,487,460],{"class":145},[124,489,463],{"class":141},[124,491,492],{"class":145}," .env",[124,494,468],{"class":141},[10,496,497],{},"After scrubbing, force-push to all remotes and notify any collaborators to re-clone. If the repo was public at any point, treat the key as compromised regardless of whether you can confirm it was accessed.",[10,499,500,501,504,505,508],{},"Add all ",[14,502,503],{},".env"," variants to ",[14,506,507],{},".gitignore",":",[115,510,512],{"className":117,"code":511,"language":119,"meta":120,"style":120},"echo \".env\" >> .gitignore\necho \".env.local\" >> .gitignore\necho \".env.*.local\" >> .gitignore\necho \".env.production\" >> .gitignore\ngit add .gitignore && git commit -m \"chore: gitignore .env files\"\n",[14,513,514,528,539,550,561],{"__ignoreMap":120},[124,515,516,519,522,525],{"class":126,"line":127},[124,517,518],{"class":141},"echo",[124,520,521],{"class":145}," \".env\"",[124,523,524],{"class":214}," >>",[124,526,527],{"class":145}," .gitignore\n",[124,529,530,532,535,537],{"class":126,"line":134},[124,531,518],{"class":141},[124,533,534],{"class":145}," \".env.local\"",[124,536,524],{"class":214},[124,538,527],{"class":145},[124,540,541,543,546,548],{"class":126,"line":155},[124,542,518],{"class":141},[124,544,545],{"class":145}," \".env.*.local\"",[124,547,524],{"class":214},[124,549,527],{"class":145},[124,551,552,554,557,559],{"class":126,"line":182},[124,553,518],{"class":141},[124,555,556],{"class":145}," \".env.production\"",[124,558,524],{"class":214},[124,560,527],{"class":145},[124,562,563,565,568,571,575,577,580,583],{"class":126,"line":191},[124,564,308],{"class":137},[124,566,567],{"class":145}," add",[124,569,570],{"class":145}," .gitignore",[124,572,574],{"class":573},"sVt8B"," && ",[124,576,308],{"class":137},[124,578,579],{"class":145}," commit",[124,581,582],{"class":141}," -m",[124,584,585],{"class":145}," \"chore: gitignore .env files\"\n",[42,587,589],{"id":588},"step-4-fix-the-root-cause-next_public_-prefix","Step 4: Fix the Root Cause (NEXT_PUBLIC_ Prefix)",[10,591,592],{},"Rotating fixes the immediate risk. Fixing the root cause stops it from happening again on the next deploy.",[594,595,597],"h3",{"id":596},"why-next_public_-variables-are-dangerous","Why NEXT_PUBLIC_ Variables Are Dangerous",[10,599,600,601,603],{},"Next.js embeds every variable prefixed with ",[14,602,20],{}," into the client bundle at build time. The prefix is designed to share configuration values with browser code: public API base URLs, feature flags, analytics IDs. Using it with a secret key is the wrong tool for the job.",[10,605,606],{},[52,607,608],{},"Wrong (key ends up in every visitor's browser):",[115,610,614],{"className":611,"code":612,"language":613,"meta":120,"style":120},"language-typescript shiki shiki-themes github-light github-dark","// app/components/ChatWidget.tsx - generated by v0\n\"use client\";\n\nexport function ChatWidget() {\n  const handleSubmit = async (message: string) => {\n    const res = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ model: \"gpt-4o\", messages: [{ role: \"user\", content: message }] }),\n    });\n    // ...\n  };\n}\n","typescript",[14,615,616,621,629,633,647,682,707,718,724,751,765,771,800,806,812,818],{"__ignoreMap":120},[124,617,618],{"class":126,"line":127},[124,619,620],{"class":130},"// app/components/ChatWidget.tsx - generated by v0\n",[124,622,623,626],{"class":126,"line":134},[124,624,625],{"class":145},"\"use client\"",[124,627,628],{"class":573},";\n",[124,630,631],{"class":126,"line":155},[124,632,195],{"emptyLinePlaceholder":194},[124,634,635,638,641,644],{"class":126,"line":182},[124,636,637],{"class":214},"export",[124,639,640],{"class":214}," function",[124,642,643],{"class":137}," ChatWidget",[124,645,646],{"class":573},"() {\n",[124,648,649,652,655,658,661,664,668,670,673,676,679],{"class":126,"line":191},[124,650,651],{"class":214},"  const",[124,653,654],{"class":137}," handleSubmit",[124,656,657],{"class":214}," =",[124,659,660],{"class":214}," async",[124,662,663],{"class":573}," (",[124,665,667],{"class":666},"s4XuR","message",[124,669,508],{"class":214},[124,671,672],{"class":141}," string",[124,674,675],{"class":573},") ",[124,677,678],{"class":214},"=>",[124,680,681],{"class":573}," {\n",[124,683,684,687,690,692,695,698,701,704],{"class":126,"line":198},[124,685,686],{"class":214},"    const",[124,688,689],{"class":141}," res",[124,691,657],{"class":214},[124,693,694],{"class":214}," await",[124,696,697],{"class":137}," fetch",[124,699,700],{"class":573},"(",[124,702,703],{"class":145},"\"https://api.openai.com/v1/chat/completions\"",[124,705,706],{"class":573},", {\n",[124,708,709,712,715],{"class":126,"line":204},[124,710,711],{"class":573},"      method: ",[124,713,714],{"class":145},"\"POST\"",[124,716,717],{"class":573},",\n",[124,719,721],{"class":126,"line":720},8,[124,722,723],{"class":573},"      headers: {\n",[124,725,727,730,733,736,739,742,744,746,749],{"class":126,"line":726},9,[124,728,729],{"class":573},"        Authorization: ",[124,731,732],{"class":145},"`Bearer ${",[124,734,735],{"class":573},"process",[124,737,738],{"class":145},".",[124,740,741],{"class":573},"env",[124,743,738],{"class":145},[124,745,16],{"class":141},[124,747,748],{"class":145},"}`",[124,750,717],{"class":573},[124,752,754,757,760,763],{"class":126,"line":753},10,[124,755,756],{"class":145},"        \"Content-Type\"",[124,758,759],{"class":573},": ",[124,761,762],{"class":145},"\"application/json\"",[124,764,717],{"class":573},[124,766,768],{"class":126,"line":767},11,[124,769,770],{"class":573},"      },\n",[124,772,774,777,780,782,785,788,791,794,797],{"class":126,"line":773},12,[124,775,776],{"class":573},"      body: ",[124,778,779],{"class":141},"JSON",[124,781,738],{"class":573},[124,783,784],{"class":137},"stringify",[124,786,787],{"class":573},"({ model: ",[124,789,790],{"class":145},"\"gpt-4o\"",[124,792,793],{"class":573},", messages: [{ role: ",[124,795,796],{"class":145},"\"user\"",[124,798,799],{"class":573},", content: message }] }),\n",[124,801,803],{"class":126,"line":802},13,[124,804,805],{"class":573},"    });\n",[124,807,809],{"class":126,"line":808},14,[124,810,811],{"class":130},"    // ...\n",[124,813,815],{"class":126,"line":814},15,[124,816,817],{"class":573},"  };\n",[124,819,821],{"class":126,"line":820},16,[124,822,823],{"class":573},"}\n",[10,825,826],{},[52,827,828],{},"Right (API call moved to a Route Handler, key stays server-side):",[115,830,832],{"className":611,"code":831,"language":613,"meta":120,"style":120},"// app/api/chat/route.ts - server-side only, key never reaches browser\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport async function POST(req: Request) {\n  const { message } = await req.json();\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: message }],\n  });\n  return Response.json({ reply: response.choices[0].message.content });\n}\n",[14,833,834,839,855,859,884,888,912,938,958,967,977,982,1001],{"__ignoreMap":120},[124,835,836],{"class":126,"line":127},[124,837,838],{"class":130},"// app/api/chat/route.ts - server-side only, key never reaches browser\n",[124,840,841,844,847,850,853],{"class":126,"line":134},[124,842,843],{"class":214},"import",[124,845,846],{"class":573}," OpenAI ",[124,848,849],{"class":214},"from",[124,851,852],{"class":145}," \"openai\"",[124,854,628],{"class":573},[124,856,857],{"class":126,"line":155},[124,858,195],{"emptyLinePlaceholder":194},[124,860,861,864,867,869,872,875,878,881],{"class":126,"line":182},[124,862,863],{"class":214},"const",[124,865,866],{"class":141}," client",[124,868,657],{"class":214},[124,870,871],{"class":214}," new",[124,873,874],{"class":137}," OpenAI",[124,876,877],{"class":573},"({ apiKey: process.env.",[124,879,880],{"class":141},"OPENAI_API_KEY",[124,882,883],{"class":573}," });\n",[124,885,886],{"class":126,"line":191},[124,887,195],{"emptyLinePlaceholder":194},[124,889,890,892,894,896,899,901,904,906,909],{"class":126,"line":198},[124,891,637],{"class":214},[124,893,660],{"class":214},[124,895,640],{"class":214},[124,897,898],{"class":137}," POST",[124,900,700],{"class":573},[124,902,903],{"class":666},"req",[124,905,508],{"class":214},[124,907,908],{"class":137}," Request",[124,910,911],{"class":573},") {\n",[124,913,914,916,919,921,924,927,929,932,935],{"class":126,"line":204},[124,915,651],{"class":214},[124,917,918],{"class":573}," { ",[124,920,667],{"class":141},[124,922,923],{"class":573}," } ",[124,925,926],{"class":214},"=",[124,928,694],{"class":214},[124,930,931],{"class":573}," req.",[124,933,934],{"class":137},"json",[124,936,937],{"class":573},"();\n",[124,939,940,942,945,947,949,952,955],{"class":126,"line":720},[124,941,651],{"class":214},[124,943,944],{"class":141}," response",[124,946,657],{"class":214},[124,948,694],{"class":214},[124,950,951],{"class":573}," client.chat.completions.",[124,953,954],{"class":137},"create",[124,956,957],{"class":573},"({\n",[124,959,960,963,965],{"class":126,"line":726},[124,961,962],{"class":573},"    model: ",[124,964,790],{"class":145},[124,966,717],{"class":573},[124,968,969,972,974],{"class":126,"line":753},[124,970,971],{"class":573},"    messages: [{ role: ",[124,973,796],{"class":145},[124,975,976],{"class":573},", content: message }],\n",[124,978,979],{"class":126,"line":767},[124,980,981],{"class":573},"  });\n",[124,983,984,987,990,992,995,998],{"class":126,"line":773},[124,985,986],{"class":214},"  return",[124,988,989],{"class":573}," Response.",[124,991,934],{"class":137},[124,993,994],{"class":573},"({ reply: response.choices[",[124,996,997],{"class":141},"0",[124,999,1000],{"class":573},"].message.content });\n",[124,1002,1003],{"class":126,"line":802},[124,1004,823],{"class":573},[115,1006,1008],{"className":611,"code":1007,"language":613,"meta":120,"style":120},"// app/components/ChatWidget.tsx - client component calls YOUR route, not OpenAI directly\n\"use client\";\n\nexport function ChatWidget() {\n  const handleSubmit = async (message: string) => {\n    const res = await fetch(\"/api/chat\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ message }),\n    });\n    const data = await res.json();\n    // ...\n  };\n}\n",[14,1009,1010,1015,1021,1025,1035,1059,1078,1086,1101,1114,1118,1136,1140,1144],{"__ignoreMap":120},[124,1011,1012],{"class":126,"line":127},[124,1013,1014],{"class":130},"// app/components/ChatWidget.tsx - client component calls YOUR route, not OpenAI directly\n",[124,1016,1017,1019],{"class":126,"line":134},[124,1018,625],{"class":145},[124,1020,628],{"class":573},[124,1022,1023],{"class":126,"line":155},[124,1024,195],{"emptyLinePlaceholder":194},[124,1026,1027,1029,1031,1033],{"class":126,"line":182},[124,1028,637],{"class":214},[124,1030,640],{"class":214},[124,1032,643],{"class":137},[124,1034,646],{"class":573},[124,1036,1037,1039,1041,1043,1045,1047,1049,1051,1053,1055,1057],{"class":126,"line":191},[124,1038,651],{"class":214},[124,1040,654],{"class":137},[124,1042,657],{"class":214},[124,1044,660],{"class":214},[124,1046,663],{"class":573},[124,1048,667],{"class":666},[124,1050,508],{"class":214},[124,1052,672],{"class":141},[124,1054,675],{"class":573},[124,1056,678],{"class":214},[124,1058,681],{"class":573},[124,1060,1061,1063,1065,1067,1069,1071,1073,1076],{"class":126,"line":198},[124,1062,686],{"class":214},[124,1064,689],{"class":141},[124,1066,657],{"class":214},[124,1068,694],{"class":214},[124,1070,697],{"class":137},[124,1072,700],{"class":573},[124,1074,1075],{"class":145},"\"/api/chat\"",[124,1077,706],{"class":573},[124,1079,1080,1082,1084],{"class":126,"line":204},[124,1081,711],{"class":573},[124,1083,714],{"class":145},[124,1085,717],{"class":573},[124,1087,1088,1091,1094,1096,1098],{"class":126,"line":720},[124,1089,1090],{"class":573},"      headers: { ",[124,1092,1093],{"class":145},"\"Content-Type\"",[124,1095,759],{"class":573},[124,1097,762],{"class":145},[124,1099,1100],{"class":573}," },\n",[124,1102,1103,1105,1107,1109,1111],{"class":126,"line":726},[124,1104,776],{"class":573},[124,1106,779],{"class":141},[124,1108,738],{"class":573},[124,1110,784],{"class":137},[124,1112,1113],{"class":573},"({ message }),\n",[124,1115,1116],{"class":126,"line":753},[124,1117,805],{"class":573},[124,1119,1120,1122,1125,1127,1129,1132,1134],{"class":126,"line":767},[124,1121,686],{"class":214},[124,1123,1124],{"class":141}," data",[124,1126,657],{"class":214},[124,1128,694],{"class":214},[124,1130,1131],{"class":573}," res.",[124,1133,934],{"class":137},[124,1135,937],{"class":573},[124,1137,1138],{"class":126,"line":773},[124,1139,811],{"class":130},[124,1141,1142],{"class":126,"line":802},[124,1143,817],{"class":573},[124,1145,1146],{"class":126,"line":808},[124,1147,823],{"class":573},[10,1149,1150,1151,1154],{},"Your React component calls ",[14,1152,1153],{},"/api/chat"," on your own domain. Your Route Handler calls OpenAI. The key never reaches the browser.",[594,1156,1158],{"id":1157},"alternative-server-actions","Alternative: Server Actions",[10,1160,1161,1162,1165],{},"If you prefer Server Actions over Route Handlers, mark the action with ",[14,1163,1164],{},"\"use server\""," and the key stays on the server automatically:",[115,1167,1169],{"className":611,"code":1168,"language":613,"meta":120,"style":120},"// app/actions/chat.ts\n\"use server\";\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport async function generateReply(message: string) {\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: message }],\n  });\n  return response.choices[0].message.content;\n}\n",[14,1170,1171,1176,1182,1194,1198,1216,1220,1241,1257,1265,1273,1277,1289],{"__ignoreMap":120},[124,1172,1173],{"class":126,"line":127},[124,1174,1175],{"class":130},"// app/actions/chat.ts\n",[124,1177,1178,1180],{"class":126,"line":134},[124,1179,1164],{"class":145},[124,1181,628],{"class":573},[124,1183,1184,1186,1188,1190,1192],{"class":126,"line":155},[124,1185,843],{"class":214},[124,1187,846],{"class":573},[124,1189,849],{"class":214},[124,1191,852],{"class":145},[124,1193,628],{"class":573},[124,1195,1196],{"class":126,"line":182},[124,1197,195],{"emptyLinePlaceholder":194},[124,1199,1200,1202,1204,1206,1208,1210,1212,1214],{"class":126,"line":191},[124,1201,863],{"class":214},[124,1203,866],{"class":141},[124,1205,657],{"class":214},[124,1207,871],{"class":214},[124,1209,874],{"class":137},[124,1211,877],{"class":573},[124,1213,880],{"class":141},[124,1215,883],{"class":573},[124,1217,1218],{"class":126,"line":198},[124,1219,195],{"emptyLinePlaceholder":194},[124,1221,1222,1224,1226,1228,1231,1233,1235,1237,1239],{"class":126,"line":204},[124,1223,637],{"class":214},[124,1225,660],{"class":214},[124,1227,640],{"class":214},[124,1229,1230],{"class":137}," generateReply",[124,1232,700],{"class":573},[124,1234,667],{"class":666},[124,1236,508],{"class":214},[124,1238,672],{"class":141},[124,1240,911],{"class":573},[124,1242,1243,1245,1247,1249,1251,1253,1255],{"class":126,"line":720},[124,1244,651],{"class":214},[124,1246,944],{"class":141},[124,1248,657],{"class":214},[124,1250,694],{"class":214},[124,1252,951],{"class":573},[124,1254,954],{"class":137},[124,1256,957],{"class":573},[124,1258,1259,1261,1263],{"class":126,"line":726},[124,1260,962],{"class":573},[124,1262,790],{"class":145},[124,1264,717],{"class":573},[124,1266,1267,1269,1271],{"class":126,"line":753},[124,1268,971],{"class":573},[124,1270,796],{"class":145},[124,1272,976],{"class":573},[124,1274,1275],{"class":126,"line":767},[124,1276,981],{"class":573},[124,1278,1279,1281,1284,1286],{"class":126,"line":773},[124,1280,986],{"class":214},[124,1282,1283],{"class":573}," response.choices[",[124,1285,997],{"class":141},[124,1287,1288],{"class":573},"].message.content;\n",[124,1290,1291],{"class":126,"line":802},[124,1292,823],{"class":573},[594,1294,1296],{"id":1295},"variable-naming-in-vercel","Variable Naming in Vercel",[10,1298,1299],{},"In Vercel's environment variables panel, name your secret without any public prefix:",[1301,1302,1303,1316],"table",{},[1304,1305,1306],"thead",{},[1307,1308,1309,1313],"tr",{},[1310,1311,1312],"th",{},"Wrong",[1310,1314,1315],{},"Right",[1317,1318,1319,1330,1341],"tbody",{},[1307,1320,1321,1326],{},[1322,1323,1324],"td",{},[14,1325,16],{},[1322,1327,1328],{},[14,1329,880],{},[1307,1331,1332,1336],{},[1322,1333,1334],{},[14,1335,231],{},[1322,1337,1338],{},[14,1339,1340],{},"ANTHROPIC_API_KEY",[1307,1342,1343,1347],{},[1322,1344,1345],{},[14,1346,227],{},[1322,1348,1349],{},[14,1350,1351],{},"STRIPE_SECRET_KEY",[10,1353,1354,1355,1358],{},"Then access it in your Route Handler as ",[14,1356,1357],{},"process.env.OPENAI_API_KEY",". Vercel injects it only at runtime, only into server-side code.",[42,1360,1362],{"id":1361},"step-5-fix-supabase-service_role-misuse","Step 5: Fix Supabase service_role Misuse",[10,1364,1365,1366,1368],{},"The Supabase ",[14,1367,36],{}," key bypasses every Row Level Security policy in your database. It is designed for server-side admin operations only.",[10,1370,1371],{},"v0 occasionally generates a Supabase client in a component or hook that uses the service_role key directly. Any visitor who opens DevTools has full read and write access to your entire database.",[10,1373,1374],{},[52,1375,1376],{},"Wrong (service_role in a client component):",[115,1378,1380],{"className":611,"code":1379,"language":613,"meta":120,"style":120},"// app/components/DataTable.tsx - generated by v0\n\"use client\";\nimport { createClient } from \"@supabase/supabase-js\";\n\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // never do this\n);\n",[14,1381,1382,1387,1393,1407,1411,1426,1439,1451],{"__ignoreMap":120},[124,1383,1384],{"class":126,"line":127},[124,1385,1386],{"class":130},"// app/components/DataTable.tsx - generated by v0\n",[124,1388,1389,1391],{"class":126,"line":134},[124,1390,625],{"class":145},[124,1392,628],{"class":573},[124,1394,1395,1397,1400,1402,1405],{"class":126,"line":155},[124,1396,843],{"class":214},[124,1398,1399],{"class":573}," { createClient } ",[124,1401,849],{"class":214},[124,1403,1404],{"class":145}," \"@supabase/supabase-js\"",[124,1406,628],{"class":573},[124,1408,1409],{"class":126,"line":182},[124,1410,195],{"emptyLinePlaceholder":194},[124,1412,1413,1415,1418,1420,1423],{"class":126,"line":191},[124,1414,863],{"class":214},[124,1416,1417],{"class":141}," supabase",[124,1419,657],{"class":214},[124,1421,1422],{"class":137}," createClient",[124,1424,1425],{"class":573},"(\n",[124,1427,1428,1431,1434,1437],{"class":126,"line":198},[124,1429,1430],{"class":573},"  process.env.",[124,1432,1433],{"class":141},"NEXT_PUBLIC_SUPABASE_URL",[124,1435,1436],{"class":214},"!",[124,1438,717],{"class":573},[124,1440,1441,1443,1446,1448],{"class":126,"line":204},[124,1442,1430],{"class":573},[124,1444,1445],{"class":141},"NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY",[124,1447,1436],{"class":214},[124,1449,1450],{"class":130}," // never do this\n",[124,1452,1453],{"class":126,"line":720},[124,1454,1455],{"class":573},");\n",[10,1457,1458],{},[52,1459,1460],{},"Right (anon key client-side, service_role only in Route Handlers):",[115,1462,1464],{"className":611,"code":1463,"language":613,"meta":120,"style":120},"// app/components/DataTable.tsx - anon key, controlled by RLS\n\"use client\";\nimport { createClient } from \"@supabase/supabase-js\";\n\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // public by design\n);\n\n// app/api/admin/route.ts - service_role only here, server-side\nimport { createClient } from \"@supabase/supabase-js\";\n\nconst adminClient = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.SUPABASE_SERVICE_ROLE_KEY! // no NEXT_PUBLIC_ prefix = server-only\n);\n",[14,1465,1466,1471,1477,1489,1493,1505,1515,1526,1530,1534,1539,1551,1555,1568,1578,1590],{"__ignoreMap":120},[124,1467,1468],{"class":126,"line":127},[124,1469,1470],{"class":130},"// app/components/DataTable.tsx - anon key, controlled by RLS\n",[124,1472,1473,1475],{"class":126,"line":134},[124,1474,625],{"class":145},[124,1476,628],{"class":573},[124,1478,1479,1481,1483,1485,1487],{"class":126,"line":155},[124,1480,843],{"class":214},[124,1482,1399],{"class":573},[124,1484,849],{"class":214},[124,1486,1404],{"class":145},[124,1488,628],{"class":573},[124,1490,1491],{"class":126,"line":182},[124,1492,195],{"emptyLinePlaceholder":194},[124,1494,1495,1497,1499,1501,1503],{"class":126,"line":191},[124,1496,863],{"class":214},[124,1498,1417],{"class":141},[124,1500,657],{"class":214},[124,1502,1422],{"class":137},[124,1504,1425],{"class":573},[124,1506,1507,1509,1511,1513],{"class":126,"line":198},[124,1508,1430],{"class":573},[124,1510,1433],{"class":141},[124,1512,1436],{"class":214},[124,1514,717],{"class":573},[124,1516,1517,1519,1521,1523],{"class":126,"line":204},[124,1518,1430],{"class":573},[124,1520,289],{"class":141},[124,1522,1436],{"class":214},[124,1524,1525],{"class":130}," // public by design\n",[124,1527,1528],{"class":126,"line":720},[124,1529,1455],{"class":573},[124,1531,1532],{"class":126,"line":726},[124,1533,195],{"emptyLinePlaceholder":194},[124,1535,1536],{"class":126,"line":753},[124,1537,1538],{"class":130},"// app/api/admin/route.ts - service_role only here, server-side\n",[124,1540,1541,1543,1545,1547,1549],{"class":126,"line":767},[124,1542,843],{"class":214},[124,1544,1399],{"class":573},[124,1546,849],{"class":214},[124,1548,1404],{"class":145},[124,1550,628],{"class":573},[124,1552,1553],{"class":126,"line":773},[124,1554,195],{"emptyLinePlaceholder":194},[124,1556,1557,1559,1562,1564,1566],{"class":126,"line":802},[124,1558,863],{"class":214},[124,1560,1561],{"class":141}," adminClient",[124,1563,657],{"class":214},[124,1565,1422],{"class":137},[124,1567,1425],{"class":573},[124,1569,1570,1572,1574,1576],{"class":126,"line":808},[124,1571,1430],{"class":573},[124,1573,1433],{"class":141},[124,1575,1436],{"class":214},[124,1577,717],{"class":573},[124,1579,1580,1582,1585,1587],{"class":126,"line":814},[124,1581,1430],{"class":573},[124,1583,1584],{"class":141},"SUPABASE_SERVICE_ROLE_KEY",[124,1586,1436],{"class":214},[124,1588,1589],{"class":130}," // no NEXT_PUBLIC_ prefix = server-only\n",[124,1591,1592],{"class":126,"line":820},[124,1593,1455],{"class":573},[10,1595,1596],{},"The anon key is safe to expose because Supabase RLS policies control what it can access. Enable RLS on every table and add policies that match your auth model.",[1598,1599,1600],"warning-box",{},[10,1601,1602],{},"If you rotated your Supabase service_role key, generate a new one in the Supabase dashboard under Project Settings > API and update your Vercel environment variable. The old key stops working for all legitimate admin functions too, so update any server-side code that uses it before revoking.",[42,1604,1606],{"id":1605},"step-6-prevent-future-leaks","Step 6: Prevent Future Leaks",[594,1608,1610],{"id":1609},"pre-commit-hook-with-gitleaks","Pre-commit Hook with Gitleaks",[10,1612,1613],{},"Gitleaks scans for secrets before each commit:",[115,1615,1617],{"className":117,"code":1616,"language":119,"meta":120,"style":120},"# Install gitleaks (macOS)\nbrew install gitleaks\n\n# Test your current repo\ngitleaks detect --source . --verbose\n\n# Add a 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,1618,1619,1624,1634,1638,1643,1659,1663,1668,1685,1690,1695,1700,1705,1710,1715,1720],{"__ignoreMap":120},[124,1620,1621],{"class":126,"line":127},[124,1622,1623],{"class":130},"# Install gitleaks (macOS)\n",[124,1625,1626,1629,1631],{"class":126,"line":134},[124,1627,1628],{"class":137},"brew",[124,1630,441],{"class":145},[124,1632,1633],{"class":145}," gitleaks\n",[124,1635,1636],{"class":126,"line":155},[124,1637,195],{"emptyLinePlaceholder":194},[124,1639,1640],{"class":126,"line":182},[124,1641,1642],{"class":130},"# Test your current repo\n",[124,1644,1645,1648,1651,1654,1656],{"class":126,"line":191},[124,1646,1647],{"class":137},"gitleaks",[124,1649,1650],{"class":145}," detect",[124,1652,1653],{"class":141}," --source",[124,1655,149],{"class":145},[124,1657,1658],{"class":141}," --verbose\n",[124,1660,1661],{"class":126,"line":198},[124,1662,195],{"emptyLinePlaceholder":194},[124,1664,1665],{"class":126,"line":204},[124,1666,1667],{"class":130},"# Add a pre-commit hook\n",[124,1669,1670,1673,1676,1679,1682],{"class":126,"line":720},[124,1671,1672],{"class":137},"cat",[124,1674,1675],{"class":214}," >",[124,1677,1678],{"class":145}," .git/hooks/pre-commit",[124,1680,1681],{"class":214}," \u003C\u003C",[124,1683,1684],{"class":145}," 'EOF'\n",[124,1686,1687],{"class":126,"line":726},[124,1688,1689],{"class":145},"#!/bin/sh\n",[124,1691,1692],{"class":126,"line":753},[124,1693,1694],{"class":145},"gitleaks protect --staged --verbose\n",[124,1696,1697],{"class":126,"line":767},[124,1698,1699],{"class":145},"if [ $? -ne 0 ]; then\n",[124,1701,1702],{"class":126,"line":773},[124,1703,1704],{"class":145},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[124,1706,1707],{"class":126,"line":802},[124,1708,1709],{"class":145},"  exit 1\n",[124,1711,1712],{"class":126,"line":808},[124,1713,1714],{"class":145},"fi\n",[124,1716,1717],{"class":126,"line":814},[124,1718,1719],{"class":145},"EOF\n",[124,1721,1722,1725,1728],{"class":126,"line":820},[124,1723,1724],{"class":137},"chmod",[124,1726,1727],{"class":145}," +x",[124,1729,1730],{"class":145}," .git/hooks/pre-commit\n",[594,1732,1734],{"id":1733},"what-to-put-where","What to Put Where",[1301,1736,1737,1750],{},[1304,1738,1739],{},[1307,1740,1741,1744,1747],{},[1310,1742,1743],{},"Secret type",[1310,1745,1746],{},"Where it lives",[1310,1748,1749],{},"Accessed via",[1317,1751,1752,1765,1777,1793,1806],{},[1307,1753,1754,1757,1760],{},[1322,1755,1756],{},"OpenAI / Anthropic key",[1322,1758,1759],{},"Vercel env vars (no prefix)",[1322,1761,1762,1764],{},[14,1763,1357],{}," in a Route Handler",[1307,1766,1767,1770,1772],{},[1322,1768,1769],{},"Stripe secret key",[1322,1771,1759],{},[1322,1773,1774,1764],{},[14,1775,1776],{},"process.env.STRIPE_SECRET_KEY",[1307,1778,1779,1782,1787],{},[1322,1780,1781],{},"Supabase anon key",[1322,1783,1784,1785],{},"Vercel env vars as ",[14,1786,289],{},[1322,1788,1789,1792],{},[14,1790,1791],{},"process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY"," (public)",[1307,1794,1795,1798,1800],{},[1322,1796,1797],{},"Supabase service_role",[1322,1799,1759],{},[1322,1801,1802,1805],{},[14,1803,1804],{},"process.env.SUPABASE_SERVICE_ROLE_KEY"," in a Route Handler only",[1307,1807,1808,1811,1813],{},[1322,1809,1810],{},"Database URL",[1322,1812,1759],{},[1322,1814,1815,1818],{},[14,1816,1817],{},"process.env.DATABASE_URL"," in server code only",[1820,1821,1822,1833,1839,1845,1851],"faq-section",{},[1823,1824,1826],"faq-item",{"question":1825},"How do I know if my v0 API key is exposed?",[10,1827,1828,1829,1832],{},"Open your deployed v0 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 is in your client bundle. Also run ",[14,1830,1831],{},"grep -r 'NEXT_PUBLIC_' ."," in your codebase to catch prefix-exposed variables before the next deploy.",[1823,1834,1836],{"question":1835},"What does NEXT_PUBLIC_ have to do with API key exposure?",[10,1837,1838],{},"Next.js bakes any variable prefixed with NEXT_PUBLIC_ into your client-side JavaScript at build time. This prefix exists so browser code can read configuration values, but it also means secrets are included. Rename any secret variable by dropping the prefix and move the API call to a Route Handler or Server Action so the key stays on the server.",[1823,1840,1842],{"question":1841},"Does v0 only generate UI components, so there are no API keys to worry about?",[10,1843,1844],{},"v0 has grown beyond pure UI generation. It now writes full Next.js pages with data fetching, server actions, and third-party API integrations. When you prompt it to add AI features, payment handling, or external data sources, it generates code that touches secrets. Always audit for NEXT_PUBLIC_ prefixes before deploying.",[1823,1846,1848],{"question":1847},"Do I need to rotate my key if I am not sure it was accessed?",[10,1849,1850],{},"Yes. Rotate it regardless. Automated scanners index public GitHub repositories within minutes of a push. If the key was in your codebase at any point and the repo is public, assume it was found. Rotation takes under 5 minutes and is always worth it.",[1823,1852,1854],{"question":1853},"Does deleting the Vercel deployment fix the exposure?",[10,1855,1856],{},"No. If the key was in a public GitHub repo or cached by crawlers, deleting the deployment does not remove those copies. Rotate the key first, then clean the root cause in your source code and git history.",[1858,1859,1860,1866,1871],"related-articles",{},[1861,1862],"related-card",{"description":1863,"href":1864,"title":1865},"Security analysis of v0 by Vercel: the most common vulnerabilities in generated apps and what to fix before shipping.","/blog/is-safe/v0","Is v0 Safe?",[1861,1867],{"description":1868,"href":1869,"title":1870},"12-item pre-deployment security checklist for v0-generated components: secrets, XSS, dependencies, and input handling.","/blog/checklists/v0-security-checklist","v0 Security Checklist",[1861,1872],{"description":1873,"href":1874,"title":1875},"Cursor-specific secret exposure patterns: AI echoing keys from context, NEXT_PUBLIC_ variables, and committed .cursorrules files.","/blog/how-to/fix-cursor-api-key-exposure","How to Fix Cursor API Key Exposure",[1877,1878,1881],"cta-box",{"href":1879,"label":1880},"/","Scan Your v0 App",[10,1882,1883],{},"Check your v0 deployment for API keys in your JavaScript bundle, Supabase service_role misuse, and security header gaps. Free scan, no signup required.",[1885,1886,1887],"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 pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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":120,"searchDepth":134,"depth":134,"links":1889},[1890,1891,1892,1893,1894,1899,1900],{"id":44,"depth":134,"text":45},{"id":71,"depth":134,"text":72},{"id":365,"depth":134,"text":366},{"id":419,"depth":134,"text":420},{"id":588,"depth":134,"text":589,"children":1895},[1896,1897,1898],{"id":596,"depth":155,"text":597},{"id":1157,"depth":155,"text":1158},{"id":1295,"depth":155,"text":1296},{"id":1361,"depth":134,"text":1362},{"id":1605,"depth":134,"text":1606,"children":1901},[1902,1903],{"id":1609,"depth":155,"text":1610},{"id":1733,"depth":155,"text":1734},"how-to","2026-05-31","v0 API key exposure usually comes from a NEXT_PUBLIC_-prefixed secret baked into your Next.js client bundle, or a key hardcoded in a component that v0 generated. Step-by-step fix: find, rotate, and move secrets to Vercel server-side.",false,"md",[1910,1912,1914,1916,1918],{"question":1825,"answer":1911},"Open your deployed v0 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 is in your client bundle. Also run: grep -r 'NEXT_PUBLIC_' . in your codebase to catch prefix-exposed variables.",{"question":1835,"answer":1913},"Next.js bakes any variable prefixed with NEXT_PUBLIC_ into your client-side JavaScript at build time. This is intentional for values that should be public (like analytics keys), but it is catastrophic for secrets. If v0 generates code using NEXT_PUBLIC_OPENAI_API_KEY, every visitor to your site can read that key from the page source.",{"question":1841,"answer":1915},"v0 has evolved beyond pure UI components. It now generates full Next.js pages with data fetching, server actions, and third-party API calls. When you prompt it to add an AI feature or payment form, it often generates code that touches secrets. Always audit for NEXT_PUBLIC_ prefixes before deploying.",{"question":1847,"answer":1917},"Yes. Rotate it regardless. Automated scanners index public GitHub repos within minutes of a push. If the key was in your codebase at any point and the repo is public, assume it was found. Rotation takes under 5 minutes.",{"question":1853,"answer":1919},"No. If the key was in a public GitHub repo or cached by crawlers, deleting the deployment does not remove those copies. Rotate the key first, then clean the root cause in your source and git history.","yellow",null,"v0 api key exposure, fix v0 api key, v0 vercel secret leaked, next_public api key exposed, v0 environment variable security, vercel environment variables, nextjs api route handler",{},"v0 API key exposure fix: find leaked NEXT_PUBLIC_ secrets in your Next.js bundle, rotate them, and move them to Vercel environment variables or Route Handlers.","/blog/how-to/fix-v0-api-key-exposure","9 min read","[object Object]","HowTo",{"title":5,"description":1906},{"loc":1925},"blog/how-to/fix-v0-api-key-exposure",[],"summary_large_image","xnSolzs4jcxF3_MrXsX1ZViW4EnAMVlWQhL-e9vo5P0",1780341510707]