[{"data":1,"prerenderedAt":949},["ShallowReactive",2],{"blog-how-to/supabase-storage-rls-403":3},{"id":4,"title":5,"body":6,"category":917,"date":701,"dateModified":701,"description":918,"draft":919,"extension":920,"faq":921,"featured":919,"headerVariant":932,"image":933,"keywords":934,"meta":935,"navigation":254,"ogDescription":938,"ogTitle":933,"path":939,"readTime":940,"schemaOrg":941,"schemaType":942,"seo":943,"sitemap":944,"stem":945,"tags":946,"twitterCard":947,"__hash__":948},"blog/blog/how-to/supabase-storage-rls-403.md","Supabase Storage Upload Fails RLS But service_role Works (2026)",{"type":7,"value":8,"toc":907},"minimark",[9,22,25,55,60,63,71,140,147,158,162,173,185,200,213,217,220,284,294,302,324,334,349,353,378,399,428,437,563,575,579,600,613,616,620,646,649,652,658,662,793,872,891,903],[10,11,12,13,17,18,21],"p",{},"Your upload fails. The error says ",[14,15,16],"code",{},"new row violates row-level security policy",". You read the policy, you test it in the SQL editor, it returns true. You swap the anon key for ",[14,19,20],{},"service_role"," to check you are not going mad, and the upload works instantly.",[10,23,24],{},"That sequence has a specific ending, and we see it in scans constantly: the service key stays in the code, ships to production, and now the whole project is readable by anyone who opens the JavaScript bundle. This page is about not getting there.",[26,27,28],"tldr",{},[10,29,30,31,34,35,38,39,42,43,46,47,50,51,54],{},"As of 2026-08-24, Supabase's troubleshooting page for this error says the Storage API runs ",[14,32,33],{},"INSERT ... RETURNING *"," and tells you to add a ",[14,36,37],{},"SELECT"," policy. That is true for ",[14,40,41],{},"upsert: true"," and not for a normal upload: the Storage source only attaches ",[14,44,45],{},"RETURNING *"," to the upsert path, and Supabase's own access control guide says ",[14,48,49],{},"INSERT"," alone is what an upload needs. Adding the SELECT policy anyway widens read access on ",[14,52,53],{},"storage.objects"," and usually leaves the real bug in place.",[56,57,59],"h2",{"id":58},"first-the-status-code-is-probably-not-403","First, the status code is probably not 403",[10,61,62],{},"Almost every writeup of this error, including the title of Supabase's own troubleshooting entry, calls it a 403.",[10,64,65,66,70],{},"The status on the wire is usually ",[67,68,69],"strong",{},"400",". What you get back looks like this:",[72,73,75],"code-block",{"label":74},"What the Storage API actually returns",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-json shiki shiki-themes github-dark","{\n  \"statusCode\": \"403\",\n  \"error\": \"Unauthorized\",\n  \"message\": \"new row violates row-level security policy\"\n}\n","json","",[14,83,84,93,110,123,134],{"__ignoreMap":81},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,92],{"class":91},"s95oV","{\n",[85,94,96,100,103,107],{"class":87,"line":95},2,[85,97,99],{"class":98},"sDLfK","  \"statusCode\"",[85,101,102],{"class":91},": ",[85,104,106],{"class":105},"sU2Wk","\"403\"",[85,108,109],{"class":91},",\n",[85,111,113,116,118,121],{"class":87,"line":112},3,[85,114,115],{"class":98},"  \"error\"",[85,117,102],{"class":91},[85,119,120],{"class":105},"\"Unauthorized\"",[85,122,109],{"class":91},[85,124,126,129,131],{"class":87,"line":125},4,[85,127,128],{"class":98},"  \"message\"",[85,130,102],{"class":91},[85,132,133],{"class":105},"\"new row violates row-level security policy\"\n",[85,135,137],{"class":87,"line":136},5,[85,138,139],{"class":91},"}\n",[10,141,142,143,146],{},"The ",[14,144,145],{},"403"," is a string inside the body. The HTTP response is a 400. If you are filtering your browser's network tab by status, filter on the message text instead, or you will scroll past the failing request looking for a red 403 that never appears.",[10,148,149,150,153,154,157],{},"Supabase's Storage service also normalises the Postgres error before returning it. Postgres raises ",[14,151,152],{},"42501"," with a message ending ",[14,155,156],{},"... for table \"objects\"","; Storage maps that to the shorter string above. So the wording you see from the API and the wording in your database logs are not identical, and searching for one will not find the other.",[56,159,161],{"id":160},"the-fix-in-the-docs-and-where-it-comes-from","The fix in the docs, and where it comes from",[10,163,164,165,172],{},"Supabase's ",[166,167,171],"a",{"href":168,"rel":169},"https://supabase.com/docs/guides/troubleshooting/storage-error-403-forbidden-new-row-violates-row-level-security-policy-on-upload-a94384",[170],"nofollow","troubleshooting entry"," gives this cause:",[174,175,176],"blockquote",{},[10,177,178,179,181,182,184],{},"The Supabase Storage API executes an ",[14,180,49],{}," operation followed by a ",[14,183,45],{}," clause to provide object details back to the client.",[10,186,187,188,190,191,193,194,196,197,199],{},"And this consequence: if a ",[14,189,37],{}," policy is missing, \"the database is unable to return the row metadata,\" so the whole transaction fails. The prescribed fix is a ",[14,192,37],{}," policy on ",[14,195,53],{}," mirroring your ",[14,198,49],{}," conditions.",[10,201,202,203,206,207,212],{},"It is a plausible story. It is also the exact mechanism that bites people using ",[14,204,205],{},"supabase-js"," to insert into a normal table, where the client ",[166,208,211],{"href":209,"rel":210},"https://supabase.com/docs/reference/javascript/v1/insert",[170],"does append a select"," to return the full record. Table inserts go through PostgREST. Storage does not.",[56,214,216],{"id":215},"what-the-storage-source-actually-does","What the Storage source actually does",[10,218,219],{},"The Storage service builds two different statements, and only one of them returns anything.",[72,221,223],{"label":222},"supabase/storage, src/storage/database/pg.ts",[76,224,228],{"className":225,"code":226,"language":227,"meta":81,"style":81},"language-sql shiki shiki-themes github-dark","-- createObject: a normal upload\nINSERT INTO storage.objects (${insert.columns})\nVALUES (${insert.placeholders})\n-- no RETURNING clause\n\n-- upsertObject: upload with upsert enabled\nINSERT INTO storage.objects (${insert.columns})\nVALUES (${insert.placeholders})\nON CONFLICT (name, bucket_id) DO UPDATE SET ${updateClause}\nRETURNING *\n","sql",[14,229,230,235,240,245,250,256,262,267,272,278],{"__ignoreMap":81},[85,231,232],{"class":87,"line":88},[85,233,234],{},"-- createObject: a normal upload\n",[85,236,237],{"class":87,"line":95},[85,238,239],{},"INSERT INTO storage.objects (${insert.columns})\n",[85,241,242],{"class":87,"line":112},[85,243,244],{},"VALUES (${insert.placeholders})\n",[85,246,247],{"class":87,"line":125},[85,248,249],{},"-- no RETURNING clause\n",[85,251,252],{"class":87,"line":136},[85,253,255],{"emptyLinePlaceholder":254},true,"\n",[85,257,259],{"class":87,"line":258},6,[85,260,261],{},"-- upsertObject: upload with upsert enabled\n",[85,263,265],{"class":87,"line":264},7,[85,266,239],{},[85,268,270],{"class":87,"line":269},8,[85,271,244],{},[85,273,275],{"class":87,"line":274},9,[85,276,277],{},"ON CONFLICT (name, bucket_id) DO UPDATE SET ${updateClause}\n",[85,279,281],{"class":87,"line":280},10,[85,282,283],{},"RETURNING *\n",[10,285,286,287,289,290,293],{},"A plain upload never asks the database to hand the row back, so there is nothing for a missing ",[14,288,37],{}," policy to block. This is not a recent change that the docs failed to catch up with, either: the older knex implementation had the same split, with ",[14,291,292],{},".returning('*')"," only on the upsert method.",[10,295,164,296,301],{},[166,297,300],{"href":298,"rel":299},"https://supabase.com/docs/guides/storage/security/access-control",[170],"access control guide"," says it plainly, and contradicts the troubleshooting page:",[174,303,304],{},[10,305,306,307,309,310,312,313,316,317,319,320,323],{},"the only RLS policy required for uploading objects is to grant the ",[14,308,49],{}," permission to the ",[14,311,53],{}," table. To allow overwriting files using the ",[14,314,315],{},"upsert"," functionality you will need to additionally grant ",[14,318,37],{}," and ",[14,321,322],{},"UPDATE"," permissions.",[325,326,327],"warning-box",{},[10,328,329,330,333],{},"The troubleshooting entry also carries an unfilled template placeholder in its resolution text, telling you to add a policy to ",[14,331,332],{},"example_schema.example_table",". Read it as a low-review auto-generated page rather than maintained guidance, and prefer the access control guide when the two disagree.",[10,335,336,337,340,341,319,343,345,346,348],{},"So: if you pass ",[14,338,339],{},"{ upsert: true }",", the docs are right and you need ",[14,342,37],{},[14,344,322],{}," as well. If you do not, adding a broad ",[14,347,37],{}," policy grants every matching user read access to object metadata they previously could not see, and your upload still fails.",[56,350,352],{"id":351},"what-to-check-instead","What to check instead",[354,355,357],"step",{"number":356},"1",[10,358,359,362,363,365,366,369,370,319,372,374,375,377],{},[67,360,361],{},"Confirm whether you are upserting."," Look for ",[14,364,339],{}," in the ",[14,367,368],{},".upload()"," call. If it is there, add ",[14,371,37],{},[14,373,322],{}," policies matching your ",[14,376,49],{}," conditions and you are done.",[354,379,381],{"number":380},"2",[10,382,383,386,387,390,391,394,395,398],{},[67,384,385],{},"Stop testing the policy in the SQL editor."," This is the single most misleading step in the whole debugging loop. The editor runs without ",[14,388,389],{},"request.jwt.claims"," set, so any ",[14,392,393],{},"auth.uid()"," or JWT-claim expression in your policy evaluates in a context the real request never has. A policy that returns ",[14,396,397],{},"true"," there has told you nothing.",[354,400,402],{"number":401},"3",[10,403,404,407,408,411,412,415,416,419,420,423,424,427],{},[67,405,406],{},"Check the path, not just the user."," Storage policies almost always match on the object name, and ",[14,409,410],{},"storage.foldername(name)"," or ",[14,413,414],{},"split_part(name, '/', 1)"," against a user ID is where these break. Log the exact ",[14,417,418],{},"name"," your client sends and compare it character for character with what the policy expects. A leading slash, or ",[14,421,422],{},"userId"," where the policy wants ",[14,425,426],{},"auth.uid()::text",", fails silently as a policy mismatch.",[354,429,431],{"number":430},"4",[10,432,433,436],{},[67,434,435],{},"Reproduce with curl and a real user JWT",", not the anon key alone. That is the only way to see the policy evaluated with the same claims your app sends.",[72,438,440],{"label":439},"Reproduce the failure with the real auth context",[76,441,445],{"className":442,"code":443,"language":444,"meta":81,"style":81},"language-bash shiki shiki-themes github-dark","# Grab a real session token from your app (browser devtools, Application, Local Storage)\nTOKEN=\"eyJ...\"\nPROJECT=\"your-project-ref\"\n\ncurl -i -X POST \\\n  \"https://$PROJECT.supabase.co/storage/v1/object/your-bucket/$(uuidgen).png\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"apikey: $YOUR_ANON_KEY\" \\\n  -H \"Content-Type: image/png\" \\\n  --data-binary @test.png\n","bash",[14,446,447,453,465,475,479,497,516,532,546,555],{"__ignoreMap":81},[85,448,449],{"class":87,"line":88},[85,450,452],{"class":451},"sAwPA","# Grab a real session token from your app (browser devtools, Application, Local Storage)\n",[85,454,455,458,462],{"class":87,"line":95},[85,456,457],{"class":91},"TOKEN",[85,459,461],{"class":460},"snl16","=",[85,463,464],{"class":105},"\"eyJ...\"\n",[85,466,467,470,472],{"class":87,"line":112},[85,468,469],{"class":91},"PROJECT",[85,471,461],{"class":460},[85,473,474],{"class":105},"\"your-project-ref\"\n",[85,476,477],{"class":87,"line":125},[85,478,255],{"emptyLinePlaceholder":254},[85,480,481,485,488,491,494],{"class":87,"line":136},[85,482,484],{"class":483},"svObZ","curl",[85,486,487],{"class":98}," -i",[85,489,490],{"class":98}," -X",[85,492,493],{"class":105}," POST",[85,495,496],{"class":98}," \\\n",[85,498,499,502,505,508,511,514],{"class":87,"line":258},[85,500,501],{"class":105},"  \"https://",[85,503,504],{"class":91},"$PROJECT",[85,506,507],{"class":105},".supabase.co/storage/v1/object/your-bucket/$(",[85,509,510],{"class":483},"uuidgen",[85,512,513],{"class":105},").png\"",[85,515,496],{"class":98},[85,517,518,521,524,527,530],{"class":87,"line":264},[85,519,520],{"class":98},"  -H",[85,522,523],{"class":105}," \"Authorization: Bearer ",[85,525,526],{"class":91},"$TOKEN",[85,528,529],{"class":105},"\"",[85,531,496],{"class":98},[85,533,534,536,539,542,544],{"class":87,"line":269},[85,535,520],{"class":98},[85,537,538],{"class":105}," \"apikey: ",[85,540,541],{"class":91},"$YOUR_ANON_KEY",[85,543,529],{"class":105},[85,545,496],{"class":98},[85,547,548,550,553],{"class":87,"line":274},[85,549,520],{"class":98},[85,551,552],{"class":105}," \"Content-Type: image/png\"",[85,554,496],{"class":98},[85,556,557,560],{"class":87,"line":280},[85,558,559],{"class":98},"  --data-binary",[85,561,562],{"class":105}," @test.png\n",[354,564,566],{"number":565},"5",[10,567,568,571,572,574],{},[67,569,570],{},"Check for triggers on the storage schema."," A failing trigger can surface as a policy-shaped error that has nothing to do with your policies. One reported case of this exact message turned out to be a broken trigger, with the user already on ",[14,573,20],{}," and RLS not involved at all.",[56,576,578],{"id":577},"why-this-one-is-worth-writing-about","Why this one is worth writing about",[10,580,581,582,587,588,593,594,599],{},"Three GitHub discussions carry this shape, and none has an accepted answer: ",[166,583,586],{"href":584,"rel":585},"https://github.com/orgs/supabase/discussions/46022",[170],"#46022"," (2026-05-16), ",[166,589,592],{"href":590,"rel":591},"https://github.com/orgs/supabase/discussions/48349",[170],"#48349"," (2026-07-27), and ",[166,595,598],{"href":596,"rel":597},"https://github.com/orgs/supabase/discussions/37611",[170],"#37611"," (2025-08-01).",[10,601,602,603,605,606,612],{},"Worth being precise about what those threads do and do not show. They are not unanswered: #46022 drew two substantive diagnostic replies, and the best reply in #48349 is the ",[14,604,389],{}," point above. ",[67,607,608,609,611],{},"Nobody in any of them recommends using ",[14,610,20],{}," as the fix."," One responder raises it specifically to draw the distinction, that service_role bypasses RLS and anon does not.",[10,614,615],{},"What the threads do show is the temptation. The title of #48349 is a developer reporting that the upload \"works with service_role\" and that the anon key does not. That is a person with a deadline, an error message whose official explanation does not apply to them, and one key sitting right there that makes the problem go away.",[56,617,619],{"id":618},"what-reaching-for-the-service-key-actually-costs","What reaching for the service key actually costs",[621,622,623],"danger-box",{},[10,624,625,627,628,631,632,637,638,641,642,645],{},[14,626,20],{}," (a ",[67,629,630],{},"secret key"," in Supabase's ",[166,633,636],{"href":634,"rel":635},"https://supabase.com/docs/guides/api/api-keys",[170],"newer key format",") uses Postgres's ",[14,639,640],{},"BYPASSRLS"," attribute. That is a role attribute, not a per-table setting, so it does not just open the bucket. Supabase documents it as having \"full access to your project's data, bypassing Row Level Security,\" and service keys as \"entirely bypass",[85,643,644],{},"ing"," RLS policies, granting you unrestricted access to all Storage APIs.\"",[10,647,648],{},"Put that key in a React or Vue app and it is compiled into a JavaScript file your visitors download. Every table, every row, read and write, for anyone who opens devtools. This is the single most damaging finding we produce in scans of AI-built apps, and it does not usually arrive through carelessness. It arrives through exactly the sequence at the top of this page.",[10,650,651],{},"The anon key is genuinely safe in a browser. Supabase describes the publishable key as \"safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code,\" because RLS is what guards it. The secret key has no such property, and no policy you write applies to it.",[653,654,655],"tip-box",{},[10,656,657],{},"If you already shipped one, treat it as burned: rotate it in the dashboard first, then fetch your deployed bundles and grep for JWT-shaped strings to confirm nothing else went out with it. Rotation before investigation, every time.",[56,659,661],{"id":660},"sources","Sources",[663,664,665,681],"table",{},[666,667,668],"thead",{},[669,670,671,675,678],"tr",{},[672,673,674],"th",{},"Claim",[672,676,677],{},"Source",[672,679,680],{},"Checked",[682,683,684,702,730,743,758,774],"tbody",{},[669,685,686,693,699],{},[687,688,689,690,692],"td",{},"Troubleshooting page blames ",[14,691,33],{}," and prescribes a SELECT policy",[687,694,695],{},[166,696,698],{"href":168,"rel":697},[170],"Supabase troubleshooting a94384",[687,700,701],{},"2026-08-24",[669,703,704,718,728],{},[687,705,706,709,710,713,714,717],{},[14,707,708],{},"createObject"," issues no ",[14,711,712],{},"RETURNING","; only ",[14,715,716],{},"upsertObject"," does",[687,719,720],{},[166,721,724,725],{"href":722,"rel":723},"https://github.com/supabase/storage/blob/master/src/storage/database/pg.ts",[170],"supabase/storage ",[14,726,727],{},"src/storage/database/pg.ts",[687,729,701],{},[669,731,732,735,741],{},[687,733,734],{},"INSERT alone is required to upload; SELECT and UPDATE only for upsert",[687,736,737],{},[166,738,740],{"href":298,"rel":739},[170],"Supabase Storage access control",[687,742,701],{},[669,744,745,750,756],{},[687,746,747,749],{},[14,748,205],{}," table inserts do append a select, which is where the mechanism is real",[687,751,752],{},[166,753,755],{"href":209,"rel":754},[170],"supabase-js insert reference",[687,757,701],{},[669,759,760,766,772],{},[687,761,762,763,765],{},"service_role uses ",[14,764,640],{},"; publishable/anon key is safe to expose",[687,767,768],{},[166,769,771],{"href":634,"rel":770},[170],"Supabase API keys",[687,773,701],{},[669,775,776,779,791],{},[687,777,778],{},"Unresolved reports of this error",[687,780,781,784,785,784,788],{},[166,782,586],{"href":584,"rel":783},[170],", ",[166,786,592],{"href":590,"rel":787},[170],[166,789,598],{"href":596,"rel":790},[170],[687,792,701],{},[794,795,796,809,825,848,863],"faq-section",{},[797,798,800],"faq-item",{"question":799},"Why does my Supabase Storage upload fail with the anon key but work with service_role?",[10,801,802,803,805,806,808],{},"Because ",[14,804,20],{}," carries Postgres's ",[14,807,640],{}," attribute, so it skips every policy you wrote rather than satisfying them. It working is not evidence your policy is correct. It is evidence your policy is being evaluated at all, and failing, which narrows the bug to the policy or to the auth context the request arrives with.",[797,810,812],{"question":811},"Is the Supabase Storage RLS upload error a 403 or a 400?",[10,813,814,815,817,818,821,822,824],{},"The HTTP status on the wire is usually ",[67,816,69],{},". The JSON body contains a ",[14,819,820],{},"statusCode"," field with the string ",[14,823,145],{}," in it, which is where the confusion comes from. If you are filtering your network tab for 403 you will miss the request entirely, so filter on the message text instead.",[797,826,828],{"question":827},"Do I need a SELECT policy on storage.objects to upload a file?",[10,829,830,831,833,834,836,837,319,839,841,842,844,845,847],{},"Not for a plain upload. Supabase's access control guide states that the only RLS policy required for uploading is ",[14,832,49],{}," on ",[14,835,53],{},", and that ",[14,838,37],{},[14,840,322],{}," are needed additionally only for ",[14,843,315],{},". Their troubleshooting page prescribes a ",[14,846,37],{}," policy for the error generally, which is broader than the access control guide and broader than the Storage source code supports.",[797,849,851],{"question":850},"Why does my policy pass in the SQL editor but fail from the app?",[10,852,853,854,856,857,859,860,862],{},"The SQL editor runs without ",[14,855,389],{}," set, so anything in your policy that reads ",[14,858,393],{}," or a JWT claim evaluates differently there than it does on a real request. A policy that returns ",[14,861,397],{}," in the editor has told you almost nothing about what happens when the Storage API runs it.",[797,864,866],{"question":865},"What happens if I ship the service_role key to fix this?",[10,867,868,869,871],{},"You expose the entire project, not just the bucket. ",[14,870,640],{}," is a Postgres role attribute rather than a per-table setting, and Supabase documents service keys as entirely bypassing RLS policies with unrestricted access to all Storage APIs. Anyone who opens your JavaScript bundle can then read and write every table you have.",[873,874,875,881,886],"related-articles",{},[876,877],"related-card",{"description":878,"href":879,"title":880},"Writing policies that scope rows to auth.uid(), and the order to enable them in.","/blog/how-to/setup-supabase-rls","How to Set Up Supabase RLS",[876,882],{"description":883,"href":884,"title":885},"Testing with a real JWT context instead of the SQL editor, which is where most false passes come from.","/blog/how-to/test-supabase-rls","How to Test Supabase RLS Policies",[876,887],{"description":888,"href":889,"title":890},"What to do when a service key has already reached your client bundle.","/blog/how-to/fix-supabase-api-key-exposure","Fix Supabase API Key Exposure",[892,893,896,900],"cta-box",{"href":894,"label":895},"/","Start Free Scan",[56,897,899],{"id":898},"is-a-service-key-in-your-bundle-right-now","Is a service key in your bundle right now?",[10,901,902],{},"We fetch your deployed JavaScript, decode every JWT in it, and tell you which ones bypass your policies.",[904,905,906],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":81,"searchDepth":95,"depth":95,"links":908},[909,910,911,912,913,914,915,916],{"id":58,"depth":95,"text":59},{"id":160,"depth":95,"text":161},{"id":215,"depth":95,"text":216},{"id":351,"depth":95,"text":352},{"id":577,"depth":95,"text":578},{"id":618,"depth":95,"text":619},{"id":660,"depth":95,"text":661},{"id":898,"depth":95,"text":899},"how-to","Your Storage upload returns a row-level security error while the same policy tests fine. Here is what the status code really is, why Supabase's own troubleshooting page sends most people to the wrong fix, and how to debug it without reaching for the service key.",false,"md",[922,924,926,928,930],{"question":799,"answer":923},"Because service_role carries Postgres's BYPASSRLS attribute, so it skips every policy you wrote rather than satisfying them. It working is not evidence your policy is correct. It is evidence your policy is being evaluated at all, and failing, which narrows the bug to the policy or to the auth context the request arrives with.",{"question":811,"answer":925},"The HTTP status on the wire is usually 400. The JSON body contains a statusCode field with the string 403 in it, which is where the confusion comes from. If you are filtering your network tab for 403 you will miss the request entirely, so filter on the message text instead.",{"question":827,"answer":927},"Not for a plain upload. Supabase's access control guide states that the only RLS policy required for uploading is INSERT on storage.objects, and that SELECT and UPDATE are needed additionally only for upsert. Their troubleshooting page prescribes a SELECT policy for the error generally, which is broader than the access control guide and broader than the Storage source code supports.",{"question":850,"answer":929},"The SQL editor runs without request.jwt.claims set, so anything in your policy that reads auth.uid() or a JWT claim evaluates differently there than it does on a real request. A policy that returns true in the editor has told you almost nothing about what happens when the Storage API runs it.",{"question":865,"answer":931},"You expose the entire project, not just the bucket. BYPASSRLS is a Postgres role attribute rather than a per-table setting, and Supabase documents service keys as entirely bypassing RLS policies with unrestricted access to all Storage APIs. Anyone who opens your JavaScript bundle can then read and write every table you have.","yellow",null,"supabase storage rls 403, new row violates row-level security policy, supabase storage upload fails anon key, supabase storage works with service role, supabase storage insert policy, storage.objects rls",{"trendTrigger":936,"trendDate":937},"supabase-storage-rls-upload-403-vs-400-select-policy-doc-mismatch","2026-07-27","The SELECT-policy fix in Supabase's troubleshooting docs only applies to upserts. For a plain upload it widens read access and leaves your bug in place.","/blog/how-to/supabase-storage-rls-403","10 min read","[object Object]","HowTo",{"title":5,"description":918},{"loc":939},"blog/how-to/supabase-storage-rls-403",[],"summary_large_image","Lynzve7Eh3ECLxdS5zhM_pQrLEIp5BBoDls6Ld-fMoI",1787602606790]