[{"data":1,"prerenderedAt":409},["ShallowReactive",2],{"blog-diagrams/supabase-request-path-rls":3},{"id":4,"title":5,"body":6,"category":381,"date":382,"dateModified":382,"description":383,"draft":384,"extension":385,"faq":386,"featured":384,"headerVariant":392,"image":393,"keywords":395,"meta":396,"navigation":175,"ogDescription":397,"ogTitle":398,"path":399,"readTime":400,"schemaOrg":401,"schemaType":402,"seo":403,"sitemap":404,"stem":405,"tags":406,"twitterCard":407,"__hash__":408},"blog/blog/diagrams/supabase-request-path-rls.md","The Supabase Request Path and Three RLS States (Diagram)",{"type":7,"value":8,"toc":375},"minimark",[9,13,16,25,30,33,45,48,55,61,67,76,80,87,90,131,135,138,310,341,359,371],[10,11,12],"p",{},"One query, one key, three tables. Which table it hits decides whether the caller gets their own rows, nothing at all, or everything you have.",[10,14,15],{},"Row Level Security gets described as a switch, and that framing is what causes the trouble. Switching it on and writing a policy are two separate actions, and a table missing either one behaves in a way people do not expect.",[17,18],"diagram",{"alt":19,"caption":20,"height":21,"href":22,"src":23,"width":24},"A flowchart. One query from the browser with the publishable key arrives at PostgREST and then splits three ways depending on the table it hits: a table with Row Level Security on and a policy written returns only your rows, a table with Row Level Security on but no policy returns nothing at all, and a table where Row Level Security was never enabled returns every row.","Same key, same query, three different tables, three very different answers.",1230,"/blog/how-to/setup-supabase-rls","supabase-request-path-rls",2028,[26,27,29],"h2",{"id":28},"reading-the-diagram","Reading the diagram",[10,31,32],{},"The red box at the top is your app running in someone else's browser. It holds the publishable key, which is exactly where that key belongs. Anyone can read it, and that is fine by design.",[10,34,35,36,40,41,44],{},"The amber diamond is PostgREST, the REST API Supabase puts in front of Postgres. Every ",[37,38,39],"code",{},".from(\"your_table\").select()"," your frontend runs becomes a request to ",[37,42,43],{},"/rest/v1/your_table"," here. This is the point people tend to skip when picturing their app: the browser is not talking to Postgres, it is talking to an HTTP API that then talks to Postgres on your behalf.",[10,46,47],{},"What happens next is not decided by the key. It is decided by the table.",[10,49,50,54],{},[51,52,53],"strong",{},"Left path, green."," Row Level Security is on and a policy is written. The policy runs, and the caller gets back the rows it allows. This is the case you meant to ship.",[10,56,57,60],{},[51,58,59],{},"Middle path, grey."," Row Level Security is on but no policy exists yet. Postgres denies by default, so nothing matches and nothing comes back. This is the one that generates support threads, because an empty array looks like a bug. It is not a bug. It is the system failing closed.",[10,62,63,66],{},[51,64,65],{},"Right path, red."," Row Level Security was never enabled on this table. There is no gate to run, so the query returns every row. The publishable key in that public browser now reads the whole table.",[68,69,70],"warning-box",{},[10,71,72,75],{},[51,73,74],{},"The dangerous path is the quiet one."," The middle path announces itself immediately: your feature is broken and you go and fix it. The right-hand path works perfectly. Your app shows the data, nothing errors, and nothing tells you that a stranger running the same query gets the same rows.",[26,77,79],{"id":78},"why-the-right-hand-path-keeps-happening","Why the right-hand path keeps happening",[10,81,82,83,86],{},"Supabase enables Row Level Security for you when you create a table through the dashboard's table editor. It does not when the table is created by a ",[37,84,85],{},"CREATE TABLE"," statement, whether you ran that in the SQL editor or it arrived in a migration.",[10,88,89],{},"That matters here because AI-generated backends create tables the second way almost every time. Ask an assistant for a schema and you get SQL, not a sequence of dashboard clicks. So the tables in a vibe-coded Supabase project start life on the right-hand path unless something explicitly moves them off it.",[91,92,93,96],"tip-box",{},[10,94,95],{},"The check takes one query. Run this in the SQL editor and every row it returns is a table sitting on the red path.",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-sql shiki shiki-themes github-dark","select tablename\nfrom pg_tables\nwhere schemaname = 'public'\n  and rowsecurity = false;\n","sql","",[37,104,105,113,119,125],{"__ignoreMap":102},[106,107,110],"span",{"class":108,"line":109},"line",1,[106,111,112],{},"select tablename\n",[106,114,116],{"class":108,"line":115},2,[106,117,118],{},"from pg_tables\n",[106,120,122],{"class":108,"line":121},3,[106,123,124],{},"where schemaname = 'public'\n",[106,126,128],{"class":108,"line":127},4,[106,129,130],{},"  and rowsecurity = false;\n",[26,132,134],{"id":133},"the-mermaid-source","The Mermaid source",[10,136,137],{},"Copy this and swap in your own tables.",[97,139,143],{"className":140,"code":141,"language":142,"meta":102,"style":102},"language-mermaid shiki shiki-themes github-dark","---\ntitle: \"One of these tables hands over every row\"\n---\nflowchart TB\n    UI[\"PUBLIC. Your app in the browser.\u003Cbr/>holds sb_publishable_...\"]\n\n    PG{\"PostgREST\u003Cbr/>/rest/v1/your_table\"}\n\n    T1[\"RLS on, policy written\u003Cbr/>the case you meant to ship\"]\n    T2[\"RLS on, no policy yet\u003Cbr/>switched on, never filled in\"]\n    T3[\"RLS never enabled\u003Cbr/>the default for a table made in SQL\"]\n\n    UI -->|\"select() from the browser\"| PG\n    PG -->|\"your rows only\"| T1\n    PG -->|\"nothing comes back\"| T2\n    PG ==>|\"no gate: \u003Cb>every row\u003C/b>\"| T3\n\n    classDef danger fill:#fef2f2,stroke:#ef4444,stroke-width:3px,color:#1c1917\n    classDef safe fill:#ecfdf5,stroke:#10b981,stroke-width:3px,color:#1c1917\n    classDef gate fill:#fffbeb,stroke:#f59e0b,stroke-width:3px,color:#1c1917\n    classDef store fill:#f5f5f4,stroke:#57534e,stroke-width:2.5px,color:#1c1917\n\n    class UI danger\n    class PG gate\n    class T1 safe\n    class T2 store\n    class T3 danger\n\n    linkStyle 3 stroke:#ef4444,stroke-width:4px\n","mermaid",[37,144,145,150,155,159,164,170,177,183,188,194,200,206,211,217,223,229,235,240,246,252,258,264,269,275,281,287,293,299,304],{"__ignoreMap":102},[106,146,147],{"class":108,"line":109},[106,148,149],{},"---\n",[106,151,152],{"class":108,"line":115},[106,153,154],{},"title: \"One of these tables hands over every row\"\n",[106,156,157],{"class":108,"line":121},[106,158,149],{},[106,160,161],{"class":108,"line":127},[106,162,163],{},"flowchart TB\n",[106,165,167],{"class":108,"line":166},5,[106,168,169],{},"    UI[\"PUBLIC. Your app in the browser.\u003Cbr/>holds sb_publishable_...\"]\n",[106,171,173],{"class":108,"line":172},6,[106,174,176],{"emptyLinePlaceholder":175},true,"\n",[106,178,180],{"class":108,"line":179},7,[106,181,182],{},"    PG{\"PostgREST\u003Cbr/>/rest/v1/your_table\"}\n",[106,184,186],{"class":108,"line":185},8,[106,187,176],{"emptyLinePlaceholder":175},[106,189,191],{"class":108,"line":190},9,[106,192,193],{},"    T1[\"RLS on, policy written\u003Cbr/>the case you meant to ship\"]\n",[106,195,197],{"class":108,"line":196},10,[106,198,199],{},"    T2[\"RLS on, no policy yet\u003Cbr/>switched on, never filled in\"]\n",[106,201,203],{"class":108,"line":202},11,[106,204,205],{},"    T3[\"RLS never enabled\u003Cbr/>the default for a table made in SQL\"]\n",[106,207,209],{"class":108,"line":208},12,[106,210,176],{"emptyLinePlaceholder":175},[106,212,214],{"class":108,"line":213},13,[106,215,216],{},"    UI -->|\"select() from the browser\"| PG\n",[106,218,220],{"class":108,"line":219},14,[106,221,222],{},"    PG -->|\"your rows only\"| T1\n",[106,224,226],{"class":108,"line":225},15,[106,227,228],{},"    PG -->|\"nothing comes back\"| T2\n",[106,230,232],{"class":108,"line":231},16,[106,233,234],{},"    PG ==>|\"no gate: \u003Cb>every row\u003C/b>\"| T3\n",[106,236,238],{"class":108,"line":237},17,[106,239,176],{"emptyLinePlaceholder":175},[106,241,243],{"class":108,"line":242},18,[106,244,245],{},"    classDef danger fill:#fef2f2,stroke:#ef4444,stroke-width:3px,color:#1c1917\n",[106,247,249],{"class":108,"line":248},19,[106,250,251],{},"    classDef safe fill:#ecfdf5,stroke:#10b981,stroke-width:3px,color:#1c1917\n",[106,253,255],{"class":108,"line":254},20,[106,256,257],{},"    classDef gate fill:#fffbeb,stroke:#f59e0b,stroke-width:3px,color:#1c1917\n",[106,259,261],{"class":108,"line":260},21,[106,262,263],{},"    classDef store fill:#f5f5f4,stroke:#57534e,stroke-width:2.5px,color:#1c1917\n",[106,265,267],{"class":108,"line":266},22,[106,268,176],{"emptyLinePlaceholder":175},[106,270,272],{"class":108,"line":271},23,[106,273,274],{},"    class UI danger\n",[106,276,278],{"class":108,"line":277},24,[106,279,280],{},"    class PG gate\n",[106,282,284],{"class":108,"line":283},25,[106,285,286],{},"    class T1 safe\n",[106,288,290],{"class":108,"line":289},26,[106,291,292],{},"    class T2 store\n",[106,294,296],{"class":108,"line":295},27,[106,297,298],{},"    class T3 danger\n",[106,300,302],{"class":108,"line":301},28,[106,303,176],{"emptyLinePlaceholder":175},[106,305,307],{"class":108,"line":306},29,[106,308,309],{},"    linkStyle 3 stroke:#ef4444,stroke-width:4px\n",[311,312,313,320,326,335],"faq-section",{},[314,315,317],"faq-item",{"question":316},"What is the difference between RLS being off and having no policy?",[10,318,319],{},"They look similar in the dashboard and behave like opposites. With Row Level Security enabled and no policy, Postgres denies everything, so your query returns an empty array. With Row Level Security never enabled, there is no check to run, so the query returns every row in the table. One fails closed, the other fails open.",[314,321,323],{"question":322},"Why did my Supabase query suddenly return an empty array?",[10,324,325],{},"Usually because Row Level Security is on for that table and no policy matches your request. That is the middle path in the diagram. It is the safe failure, and the fix is to write the policy rather than to switch Row Level Security back off.",[314,327,329],{"question":328},"Do tables I create in the SQL editor have RLS on by default?",[10,330,331,332,334],{},"No. Tables created through the dashboard's table editor get Row Level Security enabled for you, but a table created with a ",[37,333,85],{}," statement in the SQL editor or in a migration does not. AI-generated migrations produce exactly this shape, which is why the right-hand path in the diagram is worth checking for.",[314,336,338],{"question":337},"Does the publishable key being public mean my data is public?",[10,339,340],{},"Only for tables on the right-hand path. The key is designed to be readable by anyone, and Row Level Security is the thing that makes that safe. So the key sitting in your bundle is not the problem. A table with no gate in front of it is.",[342,343,344,349,354],"related-articles",{},[345,346],"related-card",{"description":347,"href":22,"title":348},"Writing the policy that moves a table onto the green path","How to Set Up Supabase RLS",[345,350],{"description":351,"href":352,"title":353},"The companion diagram: which of your two keys gets checked at all","/blog/diagrams/supabase-key-trust-boundary","Supabase Keys and the Trust Boundary",[345,355],{"description":356,"href":357,"title":358},"When the middle path is what you are actually hitting","/blog/how-to/supabase-table-not-showing-in-api","Supabase Table Not Showing in the API",[360,361,364,368],"cta-box",{"href":362,"label":363},"/","Start Free Scan",[26,365,367],{"id":366},"not-sure-which-path-your-tables-are-on","Not sure which path your tables are on?",[10,369,370],{},"A scan queries your project the way a stranger would and reports what comes back.",[372,373,374],"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);}",{"title":102,"searchDepth":115,"depth":115,"links":376},[377,378,379,380],{"id":28,"depth":115,"text":29},{"id":78,"depth":115,"text":79},{"id":133,"depth":115,"text":134},{"id":366,"depth":115,"text":367},"diagrams","2026-09-08","A diagram of what happens to a browser query at PostgREST depending on whether Row Level Security is on with a policy, on without one, or never enabled at all.",false,"md",[387,388,389,391],{"question":316,"answer":319},{"question":322,"answer":325},{"question":328,"answer":390},"No. Tables created through the dashboard's table editor get Row Level Security enabled for you, but a table created with a CREATE TABLE statement in the SQL editor or in a migration does not. AI-generated migrations produce exactly this shape, which is why the right-hand path in the diagram is worth checking for.",{"question":337,"answer":340},"blue",{"src":394,"alt":19},"https://checkyourvibe.dev/diagrams/supabase-request-path-rls.png","supabase rls diagram, postgrest request path, supabase row level security no policy, rls not enabled supabase, supabase anon key reads every row",{"ogImage":394},"Switching RLS on and writing a policy are two separate steps. A table missing either one behaves very differently.",null,"/blog/diagrams/supabase-request-path-rls","4 min read","[object Object]","Article",{"title":5,"description":383},{"loc":399},"blog/diagrams/supabase-request-path-rls",[],"summary_large_image","0Ku9SR0LOeoLiXYor59WKS3I8pi_9276x-eNagBBllQ",1789672845340]