[{"data":1,"prerenderedAt":1469},["ShallowReactive",2],{"blog-how-to/fix-supabase-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1438,"date":1439,"dateModified":1439,"description":1440,"draft":1441,"extension":1442,"faq":1443,"featured":1441,"headerVariant":1454,"image":1455,"keywords":1456,"meta":1457,"navigation":217,"ogDescription":1458,"ogTitle":1455,"path":1459,"readTime":1460,"schemaOrg":1461,"schemaType":1462,"seo":1463,"sitemap":1464,"stem":1465,"tags":1466,"twitterCard":1467,"__hash__":1468},"blog/blog/how-to/fix-supabase-api-key-exposure.md","How to Fix Supabase API Key Exposure (2026)",{"type":7,"value":8,"toc":1423},"minimark",[9,18,25,46,51,54,59,70,77,81,88,91,115,119,255,276,384,450,456,460,463,475,497,505,513,522,535,541,545,548,648,655,721,725,728,732,735,816,819,898,902,905,994,1012,1016,1019,1030,1033,1097,1100,1139,1144,1148,1153,1272,1275,1280,1354,1392,1411,1419],[10,11,12,13,17],"p",{},"The Supabase ",[14,15,16],"code",{},"service_role"," key gives whoever holds it complete, unrestricted access to your database: every row, every table, no authentication checks, no RLS policies. In our scans of apps built with Bolt, Lovable, and Cursor, finding the service_role key in a client-side JavaScript bundle is the single most common critical finding on Supabase-connected apps.",[10,19,20,21,24],{},"There's a complication that makes this harder to spot: Supabase gives every project two keys that look almost identical. Both are long JWTs starting with ",[14,22,23],{},"eyJhbGci",". Most exposure guides tell you to rotate immediately without explaining which key actually needs rotating.",[26,27,28],"tldr",{},[10,29,30,31,34,35,37,38,41,42,45],{},"Supabase has two keys: the ",[14,32,33],{},"anon"," key (intentionally public, controlled by RLS) and the ",[14,36,16],{}," key (bypasses all Row Level Security). If the key in your frontend bundle decodes to ",[14,39,40],{},"\"role\": \"service_role\"",", rotate it immediately and move it to a server-side environment variable. If it decodes to ",[14,43,44],{},"\"role\": \"anon\"",", your key exposure is less urgent but you still need to verify your RLS policies are actually restricting access.",[47,48,50],"h2",{"id":49},"two-keys-two-threat-models","Two Keys, Two Threat Models",[10,52,53],{},"Before doing anything, identify which key you're dealing with.",[55,56,58],"h3",{"id":57},"the-anon-key","The Anon Key",[10,60,61,62,65,66,69],{},"The anon key is ",[14,63,64],{},"NEXT_PUBLIC_SUPABASE_ANON_KEY"," or ",[14,67,68],{},"VITE_SUPABASE_ANON_KEY"," in most AI-generated apps. Supabase designed this key to live in your frontend. It identifies your project, but it only accesses the data your Row Level Security policies allow. A user hitting your app with the anon key can't read other users' rows as long as RLS is configured correctly.",[10,71,72,76],{},[73,74,75],"strong",{},"If only the anon key is exposed:"," Don't rotate it. Fix or verify your RLS policies instead.",[55,78,80],{"id":79},"the-service-role-key","The Service Role Key",[10,82,83,84,87],{},"This is the dangerous one. It's named ",[14,85,86],{},"SUPABASE_SERVICE_ROLE_KEY"," and it should never appear in frontend code. When you initialize a Supabase client with this key, the server treats every request as if it came from a trusted admin. RLS does not apply. Every row in every table is readable and writable.",[10,89,90],{},"AI coding tools sometimes generate code like this when you ask for a quick admin feature or a data seed script, and the key ends up in a component file that compiles into your JavaScript bundle.",[92,93,94],"danger-box",{},[10,95,96,97,99,100,103,104,107,108,107,111,114],{},"Never pass the ",[14,98,16],{}," key to ",[14,101,102],{},"createClient()"," in a file under ",[14,105,106],{},"src/",", ",[14,109,110],{},"app/",[14,112,113],{},"components/",", or any directory that gets bundled for the browser. If it's there, any visitor to your app can extract it from the bundle and query your database directly.",[47,116,118],{"id":117},"step-1-identify-which-key-is-exposed","Step 1: Identify Which Key Is Exposed",[120,121,123,128,139,155,158],"step",{"number":122},"1",[10,124,125],{},[73,126,127],{},"Decode the JWT to check the role.",[10,129,130,131,134,135,138],{},"Both Supabase keys are JWTs. The second segment contains a base64-encoded JSON payload with a ",[14,132,133],{},"role"," field. Paste this into your browser console, replacing ",[14,136,137],{},"YOUR_KEY"," with the key you found:",[140,141,146],"pre",{"className":142,"code":143,"language":144,"meta":145,"style":145},"language-javascript shiki shiki-themes github-light github-dark","JSON.parse(atob('YOUR_KEY'.split('.')[1]))\n","javascript","",[14,147,148],{"__ignoreMap":145},[149,150,153],"span",{"class":151,"line":152},"line",1,[149,154,143],{},[10,156,157],{},"You'll see output like:",[140,159,163],{"className":160,"code":161,"language":162,"meta":145,"style":145},"language-json shiki shiki-themes github-light github-dark","// anon key: not an emergency\n{ \"role\": \"anon\", \"iss\": \"supabase\", \"iat\": 1710000000 }\n\n// service_role key: rotate immediately\n{ \"role\": \"service_role\", \"iss\": \"supabase\", \"iat\": 1710000000 }\n","json",[14,164,165,171,212,219,225],{"__ignoreMap":145},[149,166,167],{"class":151,"line":152},[149,168,170],{"class":169},"sJ8bj","// anon key: not an emergency\n",[149,172,174,178,182,185,189,191,194,196,199,201,204,206,209],{"class":151,"line":173},2,[149,175,177],{"class":176},"sVt8B","{ ",[149,179,181],{"class":180},"sj4cs","\"role\"",[149,183,184],{"class":176},": ",[149,186,188],{"class":187},"sZZnC","\"anon\"",[149,190,107],{"class":176},[149,192,193],{"class":180},"\"iss\"",[149,195,184],{"class":176},[149,197,198],{"class":187},"\"supabase\"",[149,200,107],{"class":176},[149,202,203],{"class":180},"\"iat\"",[149,205,184],{"class":176},[149,207,208],{"class":180},"1710000000",[149,210,211],{"class":176}," }\n",[149,213,215],{"class":151,"line":214},3,[149,216,218],{"emptyLinePlaceholder":217},true,"\n",[149,220,222],{"class":151,"line":221},4,[149,223,224],{"class":169},"// service_role key: rotate immediately\n",[149,226,228,230,232,234,237,239,241,243,245,247,249,251,253],{"class":151,"line":227},5,[149,229,177],{"class":176},[149,231,181],{"class":180},[149,233,184],{"class":176},[149,235,236],{"class":187},"\"service_role\"",[149,238,107],{"class":176},[149,240,193],{"class":180},[149,242,184],{"class":176},[149,244,198],{"class":187},[149,246,107],{"class":176},[149,248,203],{"class":180},[149,250,184],{"class":176},[149,252,208],{"class":180},[149,254,211],{"class":176},[120,256,258,263],{"number":257},"2",[10,259,260],{},[73,261,262],{},"Check your deployed JavaScript bundle.",[10,264,265,266,268,269,272,273,275],{},"Open your live app in a browser, press F12, go to Sources, and use Ctrl+F to search for ",[14,267,16],{},". If it appears in any ",[14,270,271],{},".js"," file on your domain, the key is in your client bundle. Also search for the key value itself if you know it (it starts with ",[14,274,23],{},").",[120,277,279,284,370],{"number":278},"3",[10,280,281],{},[73,282,283],{},"Search your codebase for service_role references.",[140,285,289],{"className":286,"code":287,"language":288,"meta":145,"style":145},"language-bash shiki shiki-themes github-light github-dark","# Find service_role key usage in source files\ngrep -r \"service_role\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" --include=\"*.jsx\" --include=\"*.vue\" .\n\n# Find environment variables with public prefixes that shouldn't be public\ngrep -r \"NEXT_PUBLIC_SUPABASE_SERVICE_ROLE\\|VITE_SUPABASE_SERVICE_ROLE\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.env*\" .\n","bash",[14,290,291,296,337,341,346],{"__ignoreMap":145},[149,292,293],{"class":151,"line":152},[149,294,295],{"class":169},"# Find service_role key usage in source files\n",[149,297,298,302,305,308,311,314,316,319,321,324,326,329,331,334],{"class":151,"line":173},[149,299,301],{"class":300},"sScJk","grep",[149,303,304],{"class":180}," -r",[149,306,307],{"class":187}," \"service_role\"",[149,309,310],{"class":180}," --include=",[149,312,313],{"class":187},"\"*.ts\"",[149,315,310],{"class":180},[149,317,318],{"class":187},"\"*.tsx\"",[149,320,310],{"class":180},[149,322,323],{"class":187},"\"*.js\"",[149,325,310],{"class":180},[149,327,328],{"class":187},"\"*.jsx\"",[149,330,310],{"class":180},[149,332,333],{"class":187},"\"*.vue\"",[149,335,336],{"class":187}," .\n",[149,338,339],{"class":151,"line":214},[149,340,218],{"emptyLinePlaceholder":217},[149,342,343],{"class":151,"line":221},[149,344,345],{"class":169},"# Find environment variables with public prefixes that shouldn't be public\n",[149,347,348,350,352,355,357,359,361,363,365,368],{"class":151,"line":227},[149,349,301],{"class":300},[149,351,304],{"class":180},[149,353,354],{"class":187}," \"NEXT_PUBLIC_SUPABASE_SERVICE_ROLE\\|VITE_SUPABASE_SERVICE_ROLE\"",[149,356,310],{"class":180},[149,358,313],{"class":187},[149,360,310],{"class":180},[149,362,318],{"class":187},[149,364,310],{"class":180},[149,366,367],{"class":187},"\"*.env*\"",[149,369,336],{"class":187},[10,371,372,373,107,375,107,377,379,380,383],{},"Any hit under ",[14,374,106],{},[14,376,110],{},[14,378,113],{},", or ",[14,381,382],{},"pages/"," is a problem.",[120,385,387,392,447],{"number":386},"4",[10,388,389],{},[73,390,391],{},"Check git history for committed .env files.",[140,393,395],{"className":286,"code":394,"language":288,"meta":145,"style":145},"git log --all --full-history -- .env\ngit log --all --full-history -- .env.local\ngit log --all --full-history -- .env.production\n",[14,396,397,417,432],{"__ignoreMap":145},[149,398,399,402,405,408,411,414],{"class":151,"line":152},[149,400,401],{"class":300},"git",[149,403,404],{"class":187}," log",[149,406,407],{"class":180}," --all",[149,409,410],{"class":180}," --full-history",[149,412,413],{"class":180}," --",[149,415,416],{"class":187}," .env\n",[149,418,419,421,423,425,427,429],{"class":151,"line":173},[149,420,401],{"class":300},[149,422,404],{"class":187},[149,424,407],{"class":180},[149,426,410],{"class":180},[149,428,413],{"class":180},[149,430,431],{"class":187}," .env.local\n",[149,433,434,436,438,440,442,444],{"class":151,"line":214},[149,435,401],{"class":300},[149,437,404],{"class":187},[149,439,407],{"class":180},[149,441,410],{"class":180},[149,443,413],{"class":180},[149,445,446],{"class":187}," .env.production\n",[10,448,449],{},"If these return commits, the keys were in your repo. Anyone who cloned or forked the repo may have a copy, even if you've since removed the file.",[451,452,453],"tip-box",{},[10,454,455],{},"CheckYourVibe scans your deployed app for Supabase service_role keys in the JavaScript bundle. It decodes the JWT and confirms the role, so you don't need to guess whether the key you found is actually dangerous.",[47,457,459],{"id":458},"step-2-rotate-the-service_role-key","Step 2: Rotate the Service_Role Key",[10,461,462],{},"If your audit found the service_role key exposed, rotate it before anything else.",[120,464,465],{"number":122},[10,466,467,470,471,474],{},[73,468,469],{},"Open the Supabase dashboard."," Navigate to your project, then ",[73,472,473],{},"Project Settings > API",".",[120,476,477],{"number":257},[10,478,479,482,483,485,486,488,489,492,493,496],{},[73,480,481],{},"Copy the new key value first."," Under \"Project API keys\", you'll see both the ",[14,484,33],{}," key and the ",[14,487,16],{}," key. Click ",[73,490,491],{},"Reveal"," next to the service_role key, then click ",[73,494,495],{},"Reset",". The dashboard generates a new key immediately.",[120,498,499],{"number":278},[10,500,501,504],{},[73,502,503],{},"Update your server environment variables."," Before the old key stops working, paste the new service_role key into your server's environment variables: Vercel (Project Settings > Environment Variables), Railway (Variables panel), Render (Environment > Secret Files), or whichever platform hosts your backend.",[120,506,507],{"number":386},[10,508,509,512],{},[73,510,511],{},"Redeploy your server."," Most platforms require a new deployment to pick up the environment variable change. Trigger one.",[120,514,516],{"number":515},"5",[10,517,518,521],{},[73,519,520],{},"Confirm the old key is invalidated."," Once your new deployment is live, attempt a Supabase query using the old key. It should return a 401. If it doesn't, wait a few minutes and try again.",[120,523,525],{"number":524},"6",[10,526,527,530,531,534],{},[73,528,529],{},"Check Supabase logs for suspicious activity."," In your Supabase dashboard, go to ",[73,532,533],{},"Logs > API Logs"," and filter by the time window the key was exposed. Look for unusual queries or large data exports.",[536,537,538],"warning-box",{},[10,539,540],{},"Resetting the service_role key does not reset the anon key, and vice versa. If both were exposed, reset both. But note that resetting the anon key will break any existing user sessions (their stored JWTs reference the old key's signature). Rotate the anon key only if you have strong evidence it was misused.",[47,542,544],{"id":543},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[10,546,547],{},"Rotation stops the current key from working. But if the key was ever in a git commit, everyone who cloned the repo before the scrub may still have a copy.",[140,549,551],{"className":286,"code":550,"language":288,"meta":145,"style":145},"# Install git-filter-repo\npip install git-filter-repo\n\n# Remove .env files from all 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\n# If you committed the key directly in a config file, use path-regex\ngit filter-repo --paths-glob \"**/*supabase*key*\" --invert-paths\n",[14,552,553,558,569,573,578,594,608,622,627,633],{"__ignoreMap":145},[149,554,555],{"class":151,"line":152},[149,556,557],{"class":169},"# Install git-filter-repo\n",[149,559,560,563,566],{"class":151,"line":173},[149,561,562],{"class":300},"pip",[149,564,565],{"class":187}," install",[149,567,568],{"class":187}," git-filter-repo\n",[149,570,571],{"class":151,"line":214},[149,572,218],{"emptyLinePlaceholder":217},[149,574,575],{"class":151,"line":221},[149,576,577],{"class":169},"# Remove .env files from all history\n",[149,579,580,582,585,588,591],{"class":151,"line":227},[149,581,401],{"class":300},[149,583,584],{"class":187}," filter-repo",[149,586,587],{"class":180}," --path",[149,589,590],{"class":187}," .env",[149,592,593],{"class":180}," --invert-paths\n",[149,595,597,599,601,603,606],{"class":151,"line":596},6,[149,598,401],{"class":300},[149,600,584],{"class":187},[149,602,587],{"class":180},[149,604,605],{"class":187}," .env.local",[149,607,593],{"class":180},[149,609,611,613,615,617,620],{"class":151,"line":610},7,[149,612,401],{"class":300},[149,614,584],{"class":187},[149,616,587],{"class":180},[149,618,619],{"class":187}," .env.production",[149,621,593],{"class":180},[149,623,625],{"class":151,"line":624},8,[149,626,218],{"emptyLinePlaceholder":217},[149,628,630],{"class":151,"line":629},9,[149,631,632],{"class":169},"# If you committed the key directly in a config file, use path-regex\n",[149,634,636,638,640,643,646],{"class":151,"line":635},10,[149,637,401],{"class":300},[149,639,584],{"class":187},[149,641,642],{"class":180}," --paths-glob",[149,644,645],{"class":187}," \"**/*supabase*key*\"",[149,647,593],{"class":180},[10,649,650,651,654],{},"After scrubbing, force-push to all remotes and ask collaborators to re-clone. Add the files to ",[14,652,653],{},".gitignore",":",[140,656,658],{"className":286,"code":657,"language":288,"meta":145,"style":145},"echo \".env\" >> .gitignore\necho \".env*.local\" >> .gitignore\necho \".env.production\" >> .gitignore\ngit add .gitignore && git commit -m \"chore: gitignore .env files\"\n",[14,659,660,675,686,697],{"__ignoreMap":145},[149,661,662,665,668,672],{"class":151,"line":152},[149,663,664],{"class":180},"echo",[149,666,667],{"class":187}," \".env\"",[149,669,671],{"class":670},"szBVR"," >>",[149,673,674],{"class":187}," .gitignore\n",[149,676,677,679,682,684],{"class":151,"line":173},[149,678,664],{"class":180},[149,680,681],{"class":187}," \".env*.local\"",[149,683,671],{"class":670},[149,685,674],{"class":187},[149,687,688,690,693,695],{"class":151,"line":214},[149,689,664],{"class":180},[149,691,692],{"class":187}," \".env.production\"",[149,694,671],{"class":670},[149,696,674],{"class":187},[149,698,699,701,704,707,710,712,715,718],{"class":151,"line":221},[149,700,401],{"class":300},[149,702,703],{"class":187}," add",[149,705,706],{"class":187}," .gitignore",[149,708,709],{"class":176}," && ",[149,711,401],{"class":300},[149,713,714],{"class":187}," commit",[149,716,717],{"class":180}," -m",[149,719,720],{"class":187}," \"chore: gitignore .env files\"\n",[47,722,724],{"id":723},"step-4-fix-client-code-to-use-anon-key-with-rls","Step 4: Fix Client Code to Use Anon Key with RLS",[10,726,727],{},"Rotating the key stops the immediate problem. Fixing the code prevents it from happening again.",[55,729,731],{"id":730},"the-pattern-ai-tools-generate","The Pattern AI Tools Generate",[10,733,734],{},"Tools like Bolt and Lovable sometimes generate an \"admin\" Supabase client in a component when you ask for operations that need admin access:",[140,736,740],{"className":737,"code":738,"language":739,"meta":145,"style":145},"language-typescript shiki shiki-themes github-light github-dark","// Wrong: service_role in a React component or Vue page\nimport { createClient } from '@supabase/supabase-js';\n\nconst supabaseAdmin = createClient(\n  process.env.VITE_SUPABASE_URL!,\n  process.env.VITE_SUPABASE_SERVICE_ROLE_KEY! // this ends up in the bundle\n);\n","typescript",[14,741,742,747,764,768,785,799,811],{"__ignoreMap":145},[149,743,744],{"class":151,"line":152},[149,745,746],{"class":169},"// Wrong: service_role in a React component or Vue page\n",[149,748,749,752,755,758,761],{"class":151,"line":173},[149,750,751],{"class":670},"import",[149,753,754],{"class":176}," { createClient } ",[149,756,757],{"class":670},"from",[149,759,760],{"class":187}," '@supabase/supabase-js'",[149,762,763],{"class":176},";\n",[149,765,766],{"class":151,"line":214},[149,767,218],{"emptyLinePlaceholder":217},[149,769,770,773,776,779,782],{"class":151,"line":221},[149,771,772],{"class":670},"const",[149,774,775],{"class":180}," supabaseAdmin",[149,777,778],{"class":670}," =",[149,780,781],{"class":300}," createClient",[149,783,784],{"class":176},"(\n",[149,786,787,790,793,796],{"class":151,"line":227},[149,788,789],{"class":176},"  process.env.",[149,791,792],{"class":180},"VITE_SUPABASE_URL",[149,794,795],{"class":670},"!",[149,797,798],{"class":176},",\n",[149,800,801,803,806,808],{"class":151,"line":596},[149,802,789],{"class":176},[149,804,805],{"class":180},"VITE_SUPABASE_SERVICE_ROLE_KEY",[149,807,795],{"class":670},[149,809,810],{"class":169}," // this ends up in the bundle\n",[149,812,813],{"class":151,"line":610},[149,814,815],{"class":176},");\n",[10,817,818],{},"The fix has two parts: use the anon key in the browser, and write RLS policies so the anon key can only access what it should.",[140,820,822],{"className":737,"code":821,"language":739,"meta":145,"style":145},"// Right: anon key in client-side code, controlled by RLS\nimport { createClient } from '@supabase/supabase-js';\n\nconst supabase = createClient(\n  import.meta.env.VITE_SUPABASE_URL!,\n  import.meta.env.VITE_SUPABASE_ANON_KEY! // safe, governed by RLS policies\n);\n",[14,823,824,829,841,845,858,877,894],{"__ignoreMap":145},[149,825,826],{"class":151,"line":152},[149,827,828],{"class":169},"// Right: anon key in client-side code, controlled by RLS\n",[149,830,831,833,835,837,839],{"class":151,"line":173},[149,832,751],{"class":670},[149,834,754],{"class":176},[149,836,757],{"class":670},[149,838,760],{"class":187},[149,840,763],{"class":176},[149,842,843],{"class":151,"line":214},[149,844,218],{"emptyLinePlaceholder":217},[149,846,847,849,852,854,856],{"class":151,"line":221},[149,848,772],{"class":670},[149,850,851],{"class":180}," supabase",[149,853,778],{"class":670},[149,855,781],{"class":300},[149,857,784],{"class":176},[149,859,860,863,865,868,871,873,875],{"class":151,"line":227},[149,861,862],{"class":670},"  import",[149,864,474],{"class":176},[149,866,867],{"class":180},"meta",[149,869,870],{"class":176},".env.",[149,872,792],{"class":180},[149,874,795],{"class":670},[149,876,798],{"class":176},[149,878,879,881,883,885,887,889,891],{"class":151,"line":596},[149,880,862],{"class":670},[149,882,474],{"class":176},[149,884,867],{"class":180},[149,886,870],{"class":176},[149,888,68],{"class":180},[149,890,795],{"class":670},[149,892,893],{"class":169}," // safe, governed by RLS policies\n",[149,895,896],{"class":151,"line":610},[149,897,815],{"class":176},[55,899,901],{"id":900},"use-the-service_role-key-only-in-server-side-code","Use the Service_Role Key Only in Server-Side Code",[10,903,904],{},"For operations that actually need admin access (seed scripts, webhook handlers, admin APIs), put the service_role key in a server-side function:",[140,906,908],{"className":737,"code":907,"language":739,"meta":145,"style":145},"// server/api/admin-action.post.ts (Nuxt server route)\n// api/admin-action.ts (Vercel Route Handler)\n// supabase/functions/admin-action/index.ts (Supabase Edge Function)\n\nimport { createClient } from '@supabase/supabase-js';\n\nconst supabaseAdmin = createClient(\n  process.env.SUPABASE_URL!,\n  process.env.SUPABASE_SERVICE_ROLE_KEY! // no VITE_ or NEXT_PUBLIC_ prefix\n);\n\n// This code never runs in a browser\n",[14,909,910,915,920,925,929,941,945,957,968,979,983,988],{"__ignoreMap":145},[149,911,912],{"class":151,"line":152},[149,913,914],{"class":169},"// server/api/admin-action.post.ts (Nuxt server route)\n",[149,916,917],{"class":151,"line":173},[149,918,919],{"class":169},"// api/admin-action.ts (Vercel Route Handler)\n",[149,921,922],{"class":151,"line":214},[149,923,924],{"class":169},"// supabase/functions/admin-action/index.ts (Supabase Edge Function)\n",[149,926,927],{"class":151,"line":221},[149,928,218],{"emptyLinePlaceholder":217},[149,930,931,933,935,937,939],{"class":151,"line":227},[149,932,751],{"class":670},[149,934,754],{"class":176},[149,936,757],{"class":670},[149,938,760],{"class":187},[149,940,763],{"class":176},[149,942,943],{"class":151,"line":596},[149,944,218],{"emptyLinePlaceholder":217},[149,946,947,949,951,953,955],{"class":151,"line":610},[149,948,772],{"class":670},[149,950,775],{"class":180},[149,952,778],{"class":670},[149,954,781],{"class":300},[149,956,784],{"class":176},[149,958,959,961,964,966],{"class":151,"line":624},[149,960,789],{"class":176},[149,962,963],{"class":180},"SUPABASE_URL",[149,965,795],{"class":670},[149,967,798],{"class":176},[149,969,970,972,974,976],{"class":151,"line":629},[149,971,789],{"class":176},[149,973,86],{"class":180},[149,975,795],{"class":670},[149,977,978],{"class":169}," // no VITE_ or NEXT_PUBLIC_ prefix\n",[149,980,981],{"class":151,"line":635},[149,982,815],{"class":176},[149,984,986],{"class":151,"line":985},11,[149,987,218],{"emptyLinePlaceholder":217},[149,989,991],{"class":151,"line":990},12,[149,992,993],{"class":169},"// This code never runs in a browser\n",[10,995,996,997,999,1000,1003,1004,1007,1008,1011],{},"If you're on Next.js, store it in ",[14,998,86],{}," (no ",[14,1001,1002],{},"NEXT_PUBLIC_"," prefix). Access it only in Route Handlers or Server Actions. If you're on Vite (Lovable, most SvelteKit apps), ",[14,1005,1006],{},"VITE_","-prefixed variables go into the browser bundle by design. Store the service_role key without the prefix in a separate ",[14,1009,1010],{},".env.server"," file or pass it through a backend API.",[47,1013,1015],{"id":1014},"step-5-enable-rls-on-every-table","Step 5: Enable RLS on Every Table",[10,1017,1018],{},"Once your client code is using the anon key, RLS is what keeps the anon key safe. Without it, the anon key gives full database access to every visitor.",[10,1020,1021,1022,1025,1026,1029],{},"Open your Supabase dashboard, go to ",[73,1023,1024],{},"Database > Tables",", click each table, and check ",[73,1027,1028],{},"Row Level Security",". Enable it if it's off. Then add policies that match your app's access model.",[10,1031,1032],{},"Minimum policy for user data (users can only read their own rows):",[140,1034,1038],{"className":1035,"code":1036,"language":1037,"meta":145,"style":145},"language-sql shiki shiki-themes github-light github-dark","-- Enable RLS first\nALTER TABLE profiles ENABLE ROW LEVEL SECURITY;\n\n-- Users can read their own profile\nCREATE POLICY \"profiles_select_own\"\n  ON profiles FOR SELECT\n  USING (auth.uid() = user_id);\n\n-- Users can update their own profile\nCREATE POLICY \"profiles_update_own\"\n  ON profiles FOR UPDATE\n  USING (auth.uid() = user_id);\n","sql",[14,1039,1040,1045,1050,1054,1059,1064,1069,1074,1078,1083,1088,1093],{"__ignoreMap":145},[149,1041,1042],{"class":151,"line":152},[149,1043,1044],{},"-- Enable RLS first\n",[149,1046,1047],{"class":151,"line":173},[149,1048,1049],{},"ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;\n",[149,1051,1052],{"class":151,"line":214},[149,1053,218],{"emptyLinePlaceholder":217},[149,1055,1056],{"class":151,"line":221},[149,1057,1058],{},"-- Users can read their own profile\n",[149,1060,1061],{"class":151,"line":227},[149,1062,1063],{},"CREATE POLICY \"profiles_select_own\"\n",[149,1065,1066],{"class":151,"line":596},[149,1067,1068],{},"  ON profiles FOR SELECT\n",[149,1070,1071],{"class":151,"line":610},[149,1072,1073],{},"  USING (auth.uid() = user_id);\n",[149,1075,1076],{"class":151,"line":624},[149,1077,218],{"emptyLinePlaceholder":217},[149,1079,1080],{"class":151,"line":629},[149,1081,1082],{},"-- Users can update their own profile\n",[149,1084,1085],{"class":151,"line":635},[149,1086,1087],{},"CREATE POLICY \"profiles_update_own\"\n",[149,1089,1090],{"class":151,"line":985},[149,1091,1092],{},"  ON profiles FOR UPDATE\n",[149,1094,1095],{"class":151,"line":990},[149,1096,1073],{},[10,1098,1099],{},"For public data (anyone can read, only authenticated users can write):",[140,1101,1103],{"className":1035,"code":1102,"language":1037,"meta":145,"style":145},"CREATE POLICY \"posts_public_read\"\n  ON posts FOR SELECT\n  USING (true);\n\nCREATE POLICY \"posts_auth_insert\"\n  ON posts FOR INSERT\n  WITH CHECK (auth.uid() IS NOT NULL);\n",[14,1104,1105,1110,1115,1120,1124,1129,1134],{"__ignoreMap":145},[149,1106,1107],{"class":151,"line":152},[149,1108,1109],{},"CREATE POLICY \"posts_public_read\"\n",[149,1111,1112],{"class":151,"line":173},[149,1113,1114],{},"  ON posts FOR SELECT\n",[149,1116,1117],{"class":151,"line":214},[149,1118,1119],{},"  USING (true);\n",[149,1121,1122],{"class":151,"line":221},[149,1123,218],{"emptyLinePlaceholder":217},[149,1125,1126],{"class":151,"line":227},[149,1127,1128],{},"CREATE POLICY \"posts_auth_insert\"\n",[149,1130,1131],{"class":151,"line":596},[149,1132,1133],{},"  ON posts FOR INSERT\n",[149,1135,1136],{"class":151,"line":610},[149,1137,1138],{},"  WITH CHECK (auth.uid() IS NOT NULL);\n",[536,1140,1141],{},[10,1142,1143],{},"A table with RLS enabled but zero policies blocks all access, including from authenticated users. After enabling RLS, test that your app still functions correctly. If you see empty results where you expected data, you're missing a SELECT policy.",[47,1145,1147],{"id":1146},"step-6-prevent-future-leaks","Step 6: Prevent Future Leaks",[10,1149,1150],{},[73,1151,1152],{},"Add Gitleaks as a pre-commit hook:",[140,1154,1156],{"className":286,"code":1155,"language":288,"meta":145,"style":145},"brew install gitleaks  # or: go install github.com/zricethezav/gitleaks/v8@latest\n\n# Test your repo now\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,1157,1158,1171,1175,1180,1197,1201,1206,1223,1228,1233,1238,1243,1248,1254,1260],{"__ignoreMap":145},[149,1159,1160,1163,1165,1168],{"class":151,"line":152},[149,1161,1162],{"class":300},"brew",[149,1164,565],{"class":187},[149,1166,1167],{"class":187}," gitleaks",[149,1169,1170],{"class":169},"  # or: go install github.com/zricethezav/gitleaks/v8@latest\n",[149,1172,1173],{"class":151,"line":173},[149,1174,218],{"emptyLinePlaceholder":217},[149,1176,1177],{"class":151,"line":214},[149,1178,1179],{"class":169},"# Test your repo now\n",[149,1181,1182,1185,1188,1191,1194],{"class":151,"line":221},[149,1183,1184],{"class":300},"gitleaks",[149,1186,1187],{"class":187}," detect",[149,1189,1190],{"class":180}," --source",[149,1192,1193],{"class":187}," .",[149,1195,1196],{"class":180}," --verbose\n",[149,1198,1199],{"class":151,"line":227},[149,1200,218],{"emptyLinePlaceholder":217},[149,1202,1203],{"class":151,"line":596},[149,1204,1205],{"class":169},"# Add pre-commit hook\n",[149,1207,1208,1211,1214,1217,1220],{"class":151,"line":610},[149,1209,1210],{"class":300},"cat",[149,1212,1213],{"class":670}," >",[149,1215,1216],{"class":187}," .git/hooks/pre-commit",[149,1218,1219],{"class":670}," \u003C\u003C",[149,1221,1222],{"class":187}," 'EOF'\n",[149,1224,1225],{"class":151,"line":624},[149,1226,1227],{"class":187},"#!/bin/sh\n",[149,1229,1230],{"class":151,"line":629},[149,1231,1232],{"class":187},"gitleaks protect --staged --verbose\n",[149,1234,1235],{"class":151,"line":635},[149,1236,1237],{"class":187},"if [ $? -ne 0 ]; then\n",[149,1239,1240],{"class":151,"line":985},[149,1241,1242],{"class":187},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[149,1244,1245],{"class":151,"line":990},[149,1246,1247],{"class":187},"  exit 1\n",[149,1249,1251],{"class":151,"line":1250},13,[149,1252,1253],{"class":187},"fi\n",[149,1255,1257],{"class":151,"line":1256},14,[149,1258,1259],{"class":187},"EOF\n",[149,1261,1263,1266,1269],{"class":151,"line":1262},15,[149,1264,1265],{"class":300},"chmod",[149,1267,1268],{"class":187}," +x",[149,1270,1271],{"class":187}," .git/hooks/pre-commit\n",[10,1273,1274],{},"Gitleaks has built-in detection patterns for Supabase service_role JWTs.",[10,1276,1277],{},[73,1278,1279],{},"Environment variable checklist:",[1281,1282,1283,1299],"table",{},[1284,1285,1286],"thead",{},[1287,1288,1289,1293,1296],"tr",{},[1290,1291,1292],"th",{},"Variable",[1290,1294,1295],{},"Browser-safe?",[1290,1297,1298],{},"Storage",[1300,1301,1302,1318,1331,1343],"tbody",{},[1287,1303,1304,1312,1315],{},[1305,1306,1307,65,1310],"td",{},[14,1308,1309],{},"NEXT_PUBLIC_SUPABASE_URL",[14,1311,792],{},[1305,1313,1314],{},"Yes",[1305,1316,1317],{},"Frontend env var",[1287,1319,1320,1326,1329],{},[1305,1321,1322,65,1324],{},[14,1323,64],{},[14,1325,68],{},[1305,1327,1328],{},"Yes (with RLS)",[1305,1330,1317],{},[1287,1332,1333,1337,1340],{},[1305,1334,1335],{},[14,1336,86],{},[1305,1338,1339],{},"No",[1305,1341,1342],{},"Server-only env var",[1287,1344,1345,1350,1352],{},[1305,1346,1347,1349],{},[14,1348,963],{}," (server copy)",[1305,1351,1339],{},[1305,1353,1342],{},[1355,1356,1357,1364,1374,1380,1386],"faq-section",{},[1358,1359,1361],"faq-item",{"question":1360},"Is the Supabase anon key safe to expose?",[10,1362,1363],{},"Yes, the anon key is designed to be public-facing. It identifies your Supabase project but only accesses data your Row Level Security policies allow. An attacker with the anon key can't read other users' rows if RLS is enabled and policies are correct. The danger is the service_role key, which bypasses every RLS policy.",[1358,1365,1367],{"question":1366},"How do I tell which Supabase key is in my frontend?",[10,1368,1369,1370,1373],{},"Both keys are JWTs starting with eyJhbGci. Run this in your browser console with the key's middle segment: ",[14,1371,1372],{},"JSON.parse(atob('MIDDLE_SEGMENT'))",". You'll see a JSON object with a 'role' field. \"anon\" means check your RLS policies. \"service_role\" means rotate immediately.",[1358,1375,1377],{"question":1376},"What can someone do with my Supabase service_role key?",[10,1378,1379],{},"With the service_role key, an attacker bypasses every Row Level Security policy. They can read, write, and delete any row in any table, list all users, modify auth settings, and access storage buckets without restriction. It's equivalent to superuser access to your entire Supabase project.",[1358,1381,1383],{"question":1382},"How do I rotate Supabase API keys?",[10,1384,1385],{},"Go to Project Settings > API in the Supabase dashboard. There's a Reset button next to both keys. Clicking it immediately invalidates the old key. Update your server environment variables before clicking reset, or be ready to redeploy within seconds so your backend doesn't go down.",[1358,1387,1389],{"question":1388},"Can I use the service_role key in Supabase Edge Functions?",[10,1390,1391],{},"Yes. Supabase Edge Functions run server-side and their environment variables never reach the browser. The platform automatically injects SUPABASE_SERVICE_ROLE_KEY into Edge Functions without you needing to configure it. Use the Edge Function approach for any admin operation that needs admin access.",[1393,1394,1395,1401,1406],"related-articles",{},[1396,1397],"related-card",{"description":1398,"href":1399,"title":1400},"Step-by-step guide to enabling RLS in Supabase, writing policies, and testing access. The foundation for keeping your anon key safe.","/blog/how-to/setup-supabase-rls","How to Set Up Supabase Row Level Security",[1396,1402],{"description":1403,"href":1404,"title":1405},"Emergency guide for rotating compromised API keys without downtime. Step-by-step for Stripe, OpenAI, Supabase, and others.","/blog/how-to/rotate-api-keys","How to Rotate API Keys",[1396,1407],{"description":1408,"href":1409,"title":1410},"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",[1412,1413,1416],"cta-box",{"href":1414,"label":1415},"/","Scan Your Supabase App",[10,1417,1418],{},"Check your Supabase deployment for service_role keys in your JavaScript bundle, tables missing RLS, and open policies. Free scan, no signup required.",[1420,1421,1422],"style",{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":145,"searchDepth":173,"depth":173,"links":1424},[1425,1429,1430,1431,1432,1436,1437],{"id":49,"depth":173,"text":50,"children":1426},[1427,1428],{"id":57,"depth":214,"text":58},{"id":79,"depth":214,"text":80},{"id":117,"depth":173,"text":118},{"id":458,"depth":173,"text":459},{"id":543,"depth":173,"text":544},{"id":723,"depth":173,"text":724,"children":1433},[1434,1435],{"id":730,"depth":214,"text":731},{"id":900,"depth":214,"text":901},{"id":1014,"depth":173,"text":1015},{"id":1146,"depth":173,"text":1147},"how-to","2026-06-11","Supabase has two keys: the anon key (safe to expose) and the service_role key (bypasses all RLS). Step-by-step fix: identify which one leaked, rotate it, and move secrets server-side.",false,"md",[1444,1446,1448,1450,1452],{"question":1360,"answer":1445},"Yes, the anon key is designed to be public-facing. It identifies your Supabase project, but what it can access is controlled entirely by your Row Level Security policies. If RLS is enabled and your policies are correct, an attacker with the anon key can only read what an unauthenticated user is allowed to see. The danger is the service_role key, which bypasses every RLS policy.",{"question":1366,"answer":1447},"Both keys are JWTs starting with eyJhbGci. To check which role is encoded, run this in your browser console: JSON.parse(atob('YOUR_KEY'.split('.')[1])). You'll see a JSON object with a 'role' field. 'anon' means you're fine (check RLS). 'service_role' means rotate immediately.",{"question":1376,"answer":1449},"With the service_role key, an attacker bypasses every Row Level Security policy and can read, write, and delete any row in any table in your database. They can also list all users, modify auth settings, and access storage buckets without restriction. It's equivalent to having superuser access to your entire Supabase project.",{"question":1382,"answer":1451},"Go to the Supabase dashboard, then Project Settings > API. There's a Reset button next to both the anon key and the service_role key. Clicking it immediately invalidates the old key. Before resetting, update your server environment variables with the new value, or be ready to redeploy within seconds of clicking reset.",{"question":1388,"answer":1453},"Yes. Supabase Edge Functions run server-side and never expose their environment variables to the browser. You can safely use the service_role key there. The Supabase platform automatically injects SUPABASE_SERVICE_ROLE_KEY into Edge Functions without you needing to set it manually in the dashboard.","yellow",null,"supabase api key exposure, fix supabase api key, supabase service_role key leaked, supabase anon key vs service_role, supabase rls bypass, supabase credential rotation",{},"Supabase API key fix: find the service_role key in your frontend bundle, rotate it in the dashboard, and set up RLS so your anon key stays safe.","/blog/how-to/fix-supabase-api-key-exposure","9 min read","[object Object]","HowTo",{"title":5,"description":1440},{"loc":1459},"blog/how-to/fix-supabase-api-key-exposure",[],"summary_large_image","EQkhFUrgT891R0kQBm9vGvZb5-PZEfeLnRV5Y8YiyMg",1784736388796]