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