[{"data":1,"prerenderedAt":426},["ShallowReactive",2],{"blog-en-supabase-rls-on-but-table-still-public":3},{"id":4,"title":5,"body":6,"category":388,"cover":389,"coverAlt":389,"description":390,"draft":391,"extension":392,"faq":393,"image":406,"keywords":407,"meta":413,"navigation":414,"ogTitle":415,"path":416,"published":417,"seo":418,"stem":419,"tldr":420,"updated":424,"__hash__":425},"blog_en\u002Fblog\u002Fsupabase-rls-on-but-table-still-public.md","Supabase Row Level Security is on. Your table is still public.",{"type":7,"value":8,"toc":377},"minimark",[9,13,21,26,29,32,35,39,42,45,56,77,80,89,93,104,195,201,204,212,216,223,230,233,239,249,253,256,259,299,302,307,311,314,317,328,331,335,364],[10,11,12],"p",{},"You turned Row Level Security on because something told you to: the advisor\ninside Supabase, a checklist, a scan result, somebody in a Discord. The toggle\nis green in your dashboard now. And you have been told your table is still\nreadable by strangers.",[10,14,15,16,20],{},"Here is the part that guide after guide gets wrong: ",[17,18,19],"strong",{},"the toggle does not\nprotect anything."," It decides that your rules get checked. The rules are a\nseparate thing, you have to write them yourself, and the fastest rule to write,\nthe one that makes a broken app work again, lets everybody in.",[22,23,25],"h2",{"id":24},"row-level-security-is-on-in-supabase-how-is-my-table-still-public","Row Level Security is on in Supabase. How is my table still public?",[10,27,28],{},"Because switching it on and deciding who gets in are two different steps, and\nonly the first one is a toggle.",[10,30,31],{},"Think of the toggle as putting somebody at the door. Flipping it does not decide\nwho is allowed through. It decides that somebody is now checking a list. Your\npolicies are the list. An empty list turns everyone away; a list that says\n\"everyone\" turns nobody away. Both of those are Row Level Security switched on,\nand your dashboard shows the same green either way.",[10,33,34],{},"That is why the setting on its own answers very little, and why our scanner\nnever asks Supabase whether it is enabled. It asks the table instead. It sends\nthe request a stranger would send, using the public key that ships inside your\napp, and sees whether an answer comes back. It asks for a count rather than for\nthe rows, so it learns that the door opened without reading anything behind it.",[22,36,38],{"id":37},"the-policy-that-fixed-your-app-is-probably-the-problem","The policy that fixed your app is probably the problem",[10,40,41],{},"The moment you switch Row Level Security on, your app stops showing data, and\nwhatever you did next to get it working again is the thing worth looking at.",[10,43,44],{},"That sequence is completely normal, and it is where this goes wrong. With the\nsetting on and no policies written, Postgres (the database engine Supabase\nruns on) refuses every request by default, so your lists come back empty and your screens go blank. Something has\nto go on the list. If you asked Cursor or Lovable to fix it, or pasted the first\nsnippet that made the error stop, what you have now probably looks like this:",[46,47,52],"pre",{"className":48,"code":50,"language":51},[49],"language-text","CREATE POLICY \"Enable read access for all users\"\n  ON public.profiles\n  FOR SELECT\n  USING (true);\n","text",[53,54,50],"code",{"__ignoreMap":55},"",[10,57,58,61,62,65,66,69,70,73,74,76],{},[53,59,60],{},"USING (true)"," is the condition a row has to satisfy before the database will\nhand it over. Every row satisfies ",[53,63,64],{},"true",". There is a second detail in there that\nis easy to walk past: with no ",[53,67,68],{},"TO"," clause, a policy applies to ",[53,71,72],{},"public",", and\n",[53,75,72],{}," covers signed-in visitors and complete strangers alike.",[10,78,79],{},"So the app works again, nothing shows an error, and the table is exactly as\nreadable as it was before you started.",[10,81,82,83,88],{},"That fixes reading, and only reading. If your app also saves to this table, the\nnext thing you meet is ",[84,85,87],"a",{"href":86},"\u002Fblog\u002Fnew-row-violates-row-level-security-policy","new row violates row-level security policy",",\nwhich is the same setting refusing a write, and no read policy will clear it.",[22,90,92],{"id":91},"the-four-states-a-table-can-be-in","The four states a table can be in",[10,94,95,96,99,100,103],{},"Two of these are safe and two are not, and the toggle does not tell you which\nis which. \"Publishable key\" below is the one that belongs in your app:\n",[53,97,98],{},"sb_publishable_…"," in newer Supabase projects, ",[53,101,102],{},"anon"," in older ones.",[105,106,107,126],"table",{},[108,109,110],"thead",{},[111,112,113,117,120,123],"tr",{},[114,115,116],"th",{},"Row Level Security",[114,118,119],{},"The policy",[114,121,122],{},"Your app",[114,124,125],{},"A stranger with your Supabase publishable key",[127,128,129,148,165,179],"tbody",{},[111,130,131,135,138,141],{},[132,133,134],"td",{},"Off",[132,136,137],{},"irrelevant, none are consulted",[132,139,140],{},"Works",[132,142,143],{},[144,145,147],"key-verdict",{"type":146},"danger","Reads every row",[111,149,150,153,156,159],{},[132,151,152],{},"On",[132,154,155],{},"none written",[132,157,158],{},"Broken",[132,160,161],{},[144,162,164],{"type":163},"safe","Reads nothing",[111,166,167,169,173,175],{},[132,168,152],{},[132,170,171],{},[53,172,60],{},[132,174,140],{},[132,176,177],{},[144,178,147],{"type":146},[111,180,181,183,188,190],{},[132,182,152],{},[132,184,185],{},[53,186,187],{},"USING (auth.uid() = user_id)",[132,189,140],{},[132,191,192],{},[144,193,194],{"type":163},"Reads only their own",[196,197],"diagram",{"alt":198,"caption":199,"src":200},"Four tables side by side. The first has the switch off and all six rows readable. The second has the switch on with an empty policy box and no rows readable. The third has the switch on with a policy reading true, and all six rows readable again. The fourth has the switch on with a policy matching the row's owner, and only two rows readable.","The tick under each column does not follow the switch above it. Two of these are switched on and one of those hands over everything.","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public\u002Frls-four-states-1600x760.png",[10,202,203],{},"The two middle columns are the pair that catches people. Same setting, same\ngreen badge, opposite outcomes, and the difference is one word inside a rule\nmost people never open.",[10,205,206,207,211],{},"If you would rather not read every policy yourself, our free scan asks your\nlive database the same question a stranger would, and tells you which tables\nanswered. It takes about 20 seconds and needs no account:\n",[84,208,210],{"href":209},"\u002F#scan","scan your app",".",[22,213,215],{"id":214},"authenticated-is-not-the-same-as-yours","\"Authenticated\" is not the same as \"yours\"",[10,217,218,219,222],{},"A policy that allows ",[53,220,221],{},"authenticated"," allows everybody who has an account, which\n(if your app has an open sign-up form) is everybody who is willing to fill it\nin.",[10,224,225,226,229],{},"This is the more subtle version of the same mistake, and it survives a lot of\nreview because it looks careful. ",[53,227,228],{},"TO authenticated USING (true)"," reads like a\nrestriction, and it is one: it excludes people who never signed up. What it does\nnot do is stop one of your customers from reading another customer's rows, which\nis usually the thing you meant by \"private\".",[10,231,232],{},"The rule that does that names who the row belongs to:",[46,234,237],{"className":235,"code":236,"language":51},[49],"CREATE POLICY \"Users read their own rows\"\n  ON public.orders\n  FOR SELECT\n  TO authenticated\n  USING (auth.uid() = user_id);\n",[53,238,236],{"__ignoreMap":55},[10,240,241,244,245,248],{},[53,242,243],{},"auth.uid()"," is whoever is asking. ",[53,246,247],{},"user_id"," is the column on the row saying who\nit belongs to. The row comes back when those two match, and stays where it is\nwhen they do not.",[22,250,252],{"id":251},"how-to-check-your-own-tables-in-two-minutes","How to check your own tables in two minutes",[10,254,255],{},"Open Supabase, go to Authentication → Policies, and read the expression inside\neach policy rather than the badge next to each table.",[10,257,258],{},"Three things to look for:",[260,261,262,271,280],"ul",{},[263,264,265,270],"li",{},[17,266,267,268,211],{},"A policy whose condition is ",[53,269,64],{}," Decide, table by table, whether you\nwould be comfortable putting that data on a public page. For a list of\npublished articles, yes. For anything with a person in it, no.",[263,272,273,279],{},[17,274,275,276,278],{},"A policy with no ",[53,277,68],{}," clause."," It applies to everyone, signed in or not,\neven when the rest of the rule looks specific.",[263,281,282,285,286,289,290,293,294,298],{},[17,283,284],{},"A table with the setting on, no policies at all, and an app that somehow\nstill works."," That combination means something is reaching your data another\nway, and the usual explanation is a secret key (",[53,287,288],{},"sb_secret_…",", or\n",[53,291,292],{},"service_role"," in an older project), which ignores every policy you have\nwritten. ",[84,295,297],{"href":296},"\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend","Which API keys are safe in your frontend","\ncovers how to tell that key from the safe one.",[10,300,301],{},"The other check people reach for is opening the app in a private window without\nsigning in, and it is worth doing, but know what it proves. Your app decides\nwhat to render. Your database decides what to hand over. Those are different\ndecisions, and a stranger who skips your screens gets the second one.",[196,303],{"alt":304,"caption":305,"src":306},"Two paths to the same rows. Through the app's screens, two of six rows come back. Straight to the database address, all six come back.","Your app's screens are not the fence. The same table can show two rows through your app and hand over all six to a request that never opened it.","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public\u002Fapp-view-vs-database-1600x620.png",[22,308,310],{"id":309},"what-it-looks-like-when-this-is-happening-to-you","What it looks like when this is happening to you",[10,312,313],{},"Nothing. That is the shape of this one, and it is the reason it sits in place\nfor months.",[10,315,316],{},"No error appears in your builder. Nothing gets slower, no screen breaks, no\nemail arrives. Your app behaves exactly as it did the day you shipped it,\nbecause from your app's side nothing changed: it was always allowed to read\nthose rows. What changed is that everybody else is allowed to read them too,\nusing the key that ships in the code your site sends to every visitor.",[10,318,319,320,323,324,327],{},"When it does surface, it surfaces sideways. A customer asks how someone knew\nsomething only your app knew. An address list you never published turns up\nsomewhere. And if the permissive rule covers writing as well as reading\n(",[53,321,322],{},"FOR ALL"," rather than ",[53,325,326],{},"FOR SELECT","), then rows can be changed and removed by\nanyone too, which is the version people discover as a table that is suddenly\nempty.",[10,329,330],{},"Rules also drift. A migration, a schema change, another late-night fix that\nneeded the data to load: any of them can loosen a policy without saying so,\nwhich is why this is worth a second look later rather than one look now.\nWatching for that is part of what Reeve Care does, though a reminder in your\ncalendar does the same job.",[22,332,334],{"id":333},"what-to-do-this-week","What to do this week",[336,337,338],"key-takeaways",{},[260,339,340,343,346,355,361],{},[263,341,342],{},"Open Authentication → Policies in Supabase and read the condition on every policy, table by table. The badge on the table is not the answer.",[263,344,345],{},"For each table holding people (users, profiles, orders, messages), check the rule names the row's owner rather than allowing everyone.",[263,347,348,349,351,352,354],{},"Replace any ",[53,350,60],{}," on those tables with a rule that compares ",[53,353,243],{}," to the row's owner column, and confirm your app still loads afterwards.",[263,356,357,358,360],{},"Add the ",[53,359,68],{}," clause you meant. A policy without one applies to strangers as well as to signed-in visitors.",[263,362,363],{},"If a table has the setting on, no policies, and your app still shows its data, find out what is bypassing it before you touch anything else.",[10,365,366,367,371,372,376],{},"Start with the table that would embarrass you most if it were a public page,\nand fix that one today. The ",[84,368,370],{"href":369},"\u002Fchecklist","10-minute security checklist"," covers\nthis alongside the other things worth looking at in a newly launched app, and\nthe ",[84,373,375],{"href":374},"\u002Fis-your-supabase-app-safe","Supabase safety guide"," goes through what else\ntends to be left open.",{"title":55,"searchDepth":378,"depth":378,"links":379},3,[380,382,383,384,385,386,387],{"id":24,"depth":381,"text":25},2,{"id":37,"depth":381,"text":38},{"id":91,"depth":381,"text":92},{"id":214,"depth":381,"text":215},{"id":251,"depth":381,"text":252},{"id":309,"depth":381,"text":310},{"id":333,"depth":381,"text":334},"Security basics",null,"Turning on Supabase Row Level Security does not protect a table. Your policies do, and the policy that fixed your broken app may let everyone in.",false,"md",[394,397,400,403],{"q":395,"a":396},"I turned Row Level Security on and my app stopped showing data. Did I break something?","No, that is the setting working. With Row Level Security on and no policies written, Postgres (the database engine underneath Supabase) refuses every request by default, including the ones from your own app. The fix is to add a policy that describes who should see which rows. The mistake to avoid is adding one that allows everybody, because that is the version that makes the app work and leaves the table open.",{"q":398,"a":399},"Is USING (true) ever the right policy?","Yes, for data that is genuinely public. A table of published blog posts, a product catalogue, a list of venues on a map: those are meant to be readable by anyone, and a policy allowing everyone to read them is correct. It stops being correct the moment the table has people in it. Ask yourself whether you would be comfortable posting the contents of that table on a public page, and let the answer decide the policy.",{"q":401,"a":402},"Does Row Level Security protect me if my secret key leaked?","No. A Supabase secret key (sb_secret_ in newer projects, service_role in older ones) bypasses Row Level Security completely, which is what it is for. Every policy you wrote is skipped, on every table. If that key is in your frontend, your policies are not doing anything for you and rotating the key is the first job, ahead of any policy work.",{"q":404,"a":405},"Do I need Row Level Security if my app already has a login screen?","Yes. Your login screen controls your app, and your app is not the only way to reach your database. Supabase gives every project a web address that answers requests directly, and the key needed to talk to it is in the code your app sends to every visitor. Policies are the part that applies no matter which door the request came through.","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public\u002Fcard-800x500.png",[408,409,410,411,412],"supabase rls","row level security policy","supabase table public","rls not working","supabase security",{},true,"Supabase RLS is on. Your table is still public.","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public","2026-08-10",{"title":5,"description":390},"blog\u002Fsupabase-rls-on-but-table-still-public",[421,422,423],"In Supabase, the switch and the rules are two separate things. Switched on with no rule blocks everyone; switched on with the wrong rule blocks nobody.","The rule that makes a broken app work again is usually the one that allows every request, from anyone.","Read the policy, not the toggle. One word in it decides whether strangers can read your table.","2026-08-12","3RhN8--WvtaY39Z_mlGu5F1275lBPNYhzHLrk_6mPJI",1787826048204]