[{"data":1,"prerenderedAt":1046},["ShallowReactive",2],{"blog-how-to/supabase-auth-uid-null-after-key-rotation":3},{"id":4,"title":5,"body":6,"category":1012,"date":1013,"dateModified":1013,"description":1014,"draft":1015,"extension":1016,"faq":1017,"featured":1015,"headerVariant":1029,"image":1030,"keywords":1031,"meta":1032,"navigation":541,"ogDescription":1035,"ogTitle":1030,"path":1036,"readTime":1037,"schemaOrg":1038,"schemaType":1039,"seo":1040,"sitemap":1041,"stem":1042,"tags":1043,"twitterCard":1044,"__hash__":1045},"blog/blog/how-to/supabase-auth-uid-null-after-key-rotation.md","auth.uid() Returns Null After Rotating Supabase JWT Keys",{"type":7,"value":8,"toc":1004},"minimark",[9,30,33,50,55,69,75,81,90,97,101,104,107,192,201,206,209,217,220,224,227,235,269,272,279,292,301,312,319,323,326,593,655,784,794,798,806,823,826,829,834,837,847,853,857,860,909,912,975,993,1000],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"Your user is logged in. The session is valid, the access token is in the ",[14,15,16],"code",{},"Authorization"," header, and ",[14,19,20],{},"supabase.auth.getUser()"," cheerfully returns their profile. Then every insert fails with ",[14,23,24],{},"new row violates row-level security policy",", Postgres error ",[14,27,28],{},"42501",", and updates quietly change zero rows without raising anything at all.",[10,31,32],{},"If that started right after you clicked \"Rotate keys\" in the Supabase dashboard, this page is for you. There's a fix that makes it all go away in about four seconds, and it's the worst thing you could do.",[34,35,36],"tldr",{},[10,37,38,41,42,45,46,49],{},[14,39,40],{},"auth.uid()"," returning null after migrating to asymmetric JWT signing keys means the Data API didn't verify your token and fell back to treating the request as anonymous. Do not disable Row Level Security to unblock it. That makes writes succeed and simultaneously exposes every row in the table to anyone holding your publishable key. Check first whether you needed to rotate at all: as of 2026-08-12, Supabase says rotating the legacy JWT secret is optional with no announced deadline. The late-2026 deadline you may be thinking of applies to the ",[14,43,44],{},"anon"," and ",[14,47,48],{},"service_role"," API keys, which is a different migration.",[51,52,54],"h2",{"id":53},"the-four-second-fix-that-publishes-your-database","The Four-Second Fix That Publishes Your Database",[10,56,57,58,65,66,68],{},"Here is the exact sequence, taken from a developer in Supabase ",[59,60,64],"a",{"href":61,"rel":62},"https://github.com/orgs/supabase/discussions/45812",[63],"nofollow","discussion #45812"," on 2026-05-12. They rotated from the legacy HS256 secret to ECC P-256, authenticated inserts started failing with ",[14,67,28],{},", and they tried the obvious thing:",[70,71,72],"blockquote",{},[10,73,74],{},"Disabling RLS on the table allows inserts to succeed.",[10,76,77,78,80],{},"It does. It always will. And on a Supabase project, Row Level Security is not a nice-to-have layer on top of a private database. It is the entire access control model. Your publishable key (formerly the ",[14,79,44],{}," key) is designed to sit in your frontend where anyone can read it, and the only thing standing between that key and every row in a table is the policies on that table.",[82,83,84],"danger-box",{},[10,85,86,89],{},[14,87,88],{},"ALTER TABLE your_table DISABLE ROW LEVEL SECURITY;"," converts a broken-login incident into a public-database incident. The writes start working, nothing in your app looks wrong, and your users table is now readable by anyone who opens devtools and copies the key out of your bundle. This is the single most common way a working Supabase app becomes an exposed one, and it almost always starts as someone trying to unblock a deploy at 11pm.",[10,91,92,93,96],{},"We scan for exactly this end state: tables reachable with the publishable key that return rows they shouldn't. The finding looks identical whether RLS was never enabled or was switched off during a bad afternoon. If you've already done this to restore service, turn RLS back on and roll back the key change instead. A broken login is an outage. A public ",[14,94,95],{},"users"," table is a disclosure.",[51,98,100],{"id":99},"first-check-whether-you-needed-to-rotate-at-all","First, Check Whether You Needed to Rotate at All",[10,102,103],{},"This is the part that gets skipped, and it's the one that would have prevented most of these incidents.",[10,105,106],{},"Supabase is running two separate migrations, and people are conflating them:",[108,109,110,136],"table",{},[111,112,113],"thead",{},[114,115,116,119,122],"tr",{},[117,118],"th",{},[117,120,121],{},"Legacy JWT secret to asymmetric signing keys",[117,123,124,126,127,129,130,126,133],{},[14,125,44],{}," / ",[14,128,48],{}," to ",[14,131,132],{},"sb_publishable_",[14,134,135],{},"sb_secret_",[137,138,139,151,162,173],"tbody",{},[114,140,141,145,148],{},[142,143,144],"td",{},"Mandatory?",[142,146,147],{},"No",[142,149,150],{},"Yes",[114,152,153,156,159],{},[142,154,155],{},"Deadline",[142,157,158],{},"None announced",[142,160,161],{},"Late 2026, marked TBC",[114,163,164,167,170],{},[142,165,166],{},"What it changes",[142,168,169],{},"How tokens are signed and verified",[142,171,172],{},"Which API key string your app sends",[114,174,175,178,185],{},[142,176,177],{},"Tracking",[142,179,180],{},[59,181,184],{"href":182,"rel":183},"https://supabase.com/docs/guides/auth/signing-keys",[63],"Signing keys docs",[142,186,187],{},[59,188,191],{"href":189,"rel":190},"https://github.com/orgs/supabase/discussions/29260",[63],"discussions/29260",[10,193,194,195,200],{},"The ",[59,196,199],{"href":197,"rel":198},"https://supabase.com/blog/jwt-signing-keys",[63],"announcement post"," is unambiguous about the first one:",[70,202,203],{},[10,204,205],{},"You're not required to change your JWT secret unless you choose to.",[10,207,208],{},"And the signing keys documentation says that if you aren't ready, \"you can stop here without any issue.\"",[10,210,211,212,216],{},"So the failure pattern goes like this. A founder reads that their Supabase keys are being retired by late 2026, which is true of the ",[59,213,215],{"href":214},"/blog/how-to/supabase-new-api-keys","API keys",". They open Project Settings, see a \"Migrate JWT secret\" button sitting right there, assume it's the same deadline, and click it. Two migrations, one dashboard, one panic. Only one of them was due.",[10,218,219],{},"There are real reasons to move to asymmetric keys, mainly that your services can verify tokens locally against a public key instead of calling the Auth server. It's a genuine improvement. It just isn't urgent, and it shouldn't be done in the same sitting as a deadline you misread.",[51,221,223],{"id":222},"what-the-reports-actually-say","What the Reports Actually Say",[10,225,226],{},"Being precise here matters, because the honest version of this story is narrower than the alarming version.",[10,228,229,230,234],{},"Since May 2026, at least three distinct GitHub accounts have filed reports describing the same shape of failure on ",[231,232,233],"strong",{},"hosted"," Supabase projects after migrating to asymmetric signing keys:",[236,237,238,247,261],"ul",{},[239,240,241,246],"li",{},[59,242,245],{"href":243,"rel":244},"https://github.com/supabase/supabase/issues/47621",[63],"Issue #47621"," (2026-07-05) is the clearest statement of it: \"authenticated requests resolve as anon, auth.uid() is NULL, RLS 42501 on INSERT,\" with UPDATEs silently affecting no rows while GoTrue accepts the very same token. The reporter says they reproduced it with both ES256 and a freshly rotated RS256 key, and wonders whether the verifier is still pinned to the legacy HS256 secret rather than consulting the project's JWKS. That's their hypothesis, not a confirmed cause.",[239,248,249,254,255,257,258,260],{},[59,250,253],{"href":251,"rel":252},"https://github.com/supabase/supabase/issues/48341",[63],"Issue #48341"," (2026-07-27) reports ",[14,256,40],{}," returning null despite a valid session, with ",[14,259,40],{}," working correctly when the claims are supplied manually.",[239,262,263,268],{},[59,264,267],{"href":265,"rel":266},"https://github.com/supabase/supabase/issues/48116",[63],"Issue #48116"," (2026-07-20) is a Storage upload rejected by RLS with a valid ES256 token. Worth listing, but it's the Storage enforcement path rather than the Data API, so it isn't quite the same bug.",[10,270,271],{},"Now the caveats, because they change what you should conclude.",[10,273,274,275,278],{},"All three issues were closed on the same day they were opened, and all three still carry a ",[14,276,277],{},"to-triage"," label. There is no public Supabase maintainer diagnosis of any of them, no status-page incident, and no changelog entry acknowledging a Data API verification problem. A closed issue with a triage label is not a fixed issue, but it isn't a confirmed platform bug either.",[10,280,281,282,287,288,291],{},"One nearby case did get a maintainer response, and it's a different symptom. In ",[59,283,286],{"href":284,"rel":285},"https://github.com/orgs/supabase/discussions/48246",[63],"discussion #48246"," (2026-07-23), five people reported intermittent ",[14,289,290],{},"bad_jwt: unrecognized JWT kid for algorithm ES256"," errors on Auth Admin API calls. A Supabase maintainer replied \"Should be fixed. Apologies!\" on 2026-07-25, and a reporter confirmed two clean runs of fifty requests afterward. No cause was stated publicly. A cache-desynchronisation explanation circulating in that thread came from a community member who explicitly noted they had no access to Supabase's internal infrastructure, so treat it as a guess.",[10,293,294,295,300],{},"Most recently, ",[59,296,299],{"href":297,"rel":298},"https://github.com/orgs/supabase/discussions/48902",[63],"discussion #48902"," (2026-08-10) describes GoTrue issuing an ES256 token and then returning 401 for that same token. It has community replies and no maintainer resolution as of 2026-08-12.",[302,303,304],"info-box",{},[10,305,306,307,311],{},"The honest summary as of 2026-08-12: a small number of hosted-project reports describe authenticated requests resolving as anonymous after asymmetric key migration, none has a public maintainer diagnosis, and one adjacent Admin API problem was addressed without a stated cause. That's enough to make you careful about ",[308,309,310],"em",{},"when"," you rotate. It isn't enough to tell you the platform is broken, and this post won't claim that.",[10,313,314,315,318],{},"One documented cause does exist, and it's worth ruling out first if it applies to you: on self-hosted Supabase, every service that verifies tokens (PostgREST, Realtime, Storage) must be configured with ",[14,316,317],{},"JWT_JWKS",". Miss that and you get precisely these symptoms. It doesn't explain the hosted reports above, which cite project refs, but if you self-host, start there.",[51,320,322],{"id":321},"diagnose-it-before-you-change-anything","Diagnose It Before You Change Anything",[10,324,325],{},"The reason this gets misdiagnosed is that it presents as an authorization failure and begins as an authentication one. Your policies are probably fine. Work outward from the token.",[327,328,330,335,341,572],"step",{"number":329},"1",[10,331,332],{},[231,333,334],{},"Read the token your client is actually sending.",[10,336,337,338,340],{},"Not the session object your app displays. The raw access token in the ",[14,339,16],{}," header. Grab it and decode the header and payload:",[342,343,348],"pre",{"className":344,"code":345,"language":346,"meta":347,"style":347},"language-javascript shiki shiki-themes github-dark","const { data: { session } } = await supabase.auth.getSession()\nconst [header, payload] = session.access_token\n  .split('.')\n  .slice(0, 2)\n  .map(part => JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/'))))\n\nconsole.log(header)   // { alg: \"ES256\", kid: \"...\", typ: \"JWT\" }\nconsole.log(payload)  // { sub: \"...\", role: \"authenticated\", exp: ... }\n","javascript","",[14,349,350,393,418,437,457,536,543,559],{"__ignoreMap":347},[351,352,355,359,363,367,370,374,377,380,383,386,390],"span",{"class":353,"line":354},"line",1,[351,356,358],{"class":357},"snl16","const",[351,360,362],{"class":361},"s95oV"," { ",[351,364,366],{"class":365},"s9osk","data",[351,368,369],{"class":361},": { ",[351,371,373],{"class":372},"sDLfK","session",[351,375,376],{"class":361}," } } ",[351,378,379],{"class":357},"=",[351,381,382],{"class":357}," await",[351,384,385],{"class":361}," supabase.auth.",[351,387,389],{"class":388},"svObZ","getSession",[351,391,392],{"class":361},"()\n",[351,394,396,398,401,404,407,410,413,415],{"class":353,"line":395},2,[351,397,358],{"class":357},[351,399,400],{"class":361}," [",[351,402,403],{"class":372},"header",[351,405,406],{"class":361},", ",[351,408,409],{"class":372},"payload",[351,411,412],{"class":361},"] ",[351,414,379],{"class":357},[351,416,417],{"class":361}," session.access_token\n",[351,419,421,424,427,430,434],{"class":353,"line":420},3,[351,422,423],{"class":361},"  .",[351,425,426],{"class":388},"split",[351,428,429],{"class":361},"(",[351,431,433],{"class":432},"sU2Wk","'.'",[351,435,436],{"class":361},")\n",[351,438,440,442,445,447,450,452,455],{"class":353,"line":439},4,[351,441,423],{"class":361},[351,443,444],{"class":388},"slice",[351,446,429],{"class":361},[351,448,449],{"class":372},"0",[351,451,406],{"class":361},[351,453,454],{"class":372},"2",[351,456,436],{"class":361},[351,458,460,462,465,467,470,473,476,479,482,484,487,490,493,495,498,502,504,507,509,512,515,517,519,521,524,526,528,530,533],{"class":353,"line":459},5,[351,461,423],{"class":361},[351,463,464],{"class":388},"map",[351,466,429],{"class":361},[351,468,469],{"class":365},"part",[351,471,472],{"class":357}," =>",[351,474,475],{"class":372}," JSON",[351,477,478],{"class":361},".",[351,480,481],{"class":388},"parse",[351,483,429],{"class":361},[351,485,486],{"class":388},"atob",[351,488,489],{"class":361},"(part.",[351,491,492],{"class":388},"replace",[351,494,429],{"class":361},[351,496,497],{"class":432},"/",[351,499,501],{"class":500},"sns5M","-",[351,503,497],{"class":432},[351,505,506],{"class":357},"g",[351,508,406],{"class":361},[351,510,511],{"class":432},"'+'",[351,513,514],{"class":361},").",[351,516,492],{"class":388},[351,518,429],{"class":361},[351,520,497],{"class":432},[351,522,523],{"class":500},"_",[351,525,497],{"class":432},[351,527,506],{"class":357},[351,529,406],{"class":361},[351,531,532],{"class":432},"'/'",[351,534,535],{"class":361},"))))\n",[351,537,539],{"class":353,"line":538},6,[351,540,542],{"emptyLinePlaceholder":541},true,"\n",[351,544,546,549,552,555],{"class":353,"line":545},7,[351,547,548],{"class":361},"console.",[351,550,551],{"class":388},"log",[351,553,554],{"class":361},"(header)   ",[351,556,558],{"class":557},"sAwPA","// { alg: \"ES256\", kid: \"...\", typ: \"JWT\" }\n",[351,560,562,564,566,569],{"class":353,"line":561},8,[351,563,548],{"class":361},[351,565,551],{"class":388},[351,567,568],{"class":361},"(payload)  ",[351,570,571],{"class":557},"// { sub: \"...\", role: \"authenticated\", exp: ... }\n",[10,573,574,575,578,579,582,583,586,587,589,590,592],{},"Two things to check. ",[14,576,577],{},"alg"," should match your currently active signing key, and ",[14,580,581],{},"role"," should be ",[14,584,585],{},"authenticated",". If ",[14,588,581],{}," says ",[14,591,44],{},", your user isn't logged in the way you think and this is a client-side session problem, not a key problem.",[327,594,595,604,610,645],{"number":454},[10,596,597],{},[231,598,599,600,603],{},"Confirm the token's ",[14,601,602],{},"kid"," is in the published JWKS.",[10,605,606,607,609],{},"Your project publishes its public keys. Fetch them and look for the ",[14,608,602],{}," you just read:",[342,611,615],{"className":612,"code":613,"language":614,"meta":347,"style":347},"language-bash shiki shiki-themes github-dark","curl -s \"https://\u003CPROJECT_REF>.supabase.co/auth/v1/.well-known/jwks.json\" \\\n  | grep -o '\"kid\":\"[^\"]*\"'\n","bash",[14,616,617,631],{"__ignoreMap":347},[351,618,619,622,625,628],{"class":353,"line":354},[351,620,621],{"class":388},"curl",[351,623,624],{"class":372}," -s",[351,626,627],{"class":432}," \"https://\u003CPROJECT_REF>.supabase.co/auth/v1/.well-known/jwks.json\"",[351,629,630],{"class":372}," \\\n",[351,632,633,636,639,642],{"class":353,"line":395},[351,634,635],{"class":357},"  |",[351,637,638],{"class":388}," grep",[351,640,641],{"class":372}," -o",[351,643,644],{"class":432}," '\"kid\":\"[^\"]*\"'\n",[10,646,647,648,650,651,654],{},"If the token's ",[14,649,602],{}," isn't in that list, the issuer and the verifier disagree about which key is current. That's the ",[14,652,653],{},"unrecognized JWT kid"," family of errors. Give it the cache windows described below before concluding anything, because a rotation that's minutes old will legitimately look like this.",[327,656,658,663,666,731,773],{"number":657},"3",[10,659,660],{},[231,661,662],{},"Ask the database what it sees.",[10,664,665],{},"This is the check that separates a token problem from a policy problem, and almost nobody runs it. Create a function that reports the claims as they arrived at Postgres, then call it through the Data API with the user's token:",[342,667,671],{"className":668,"code":669,"language":670,"meta":347,"style":347},"language-sql shiki shiki-themes github-dark","create or replace function public.whoami()\nreturns json\nlanguage sql\nsecurity invoker\nas $$\n  select json_build_object(\n    'uid', auth.uid(),\n    'role', auth.role(),\n    'claims', current_setting('request.jwt.claims', true)\n  );\n$$;\n","sql",[14,672,673,678,683,688,693,698,703,708,713,719,725],{"__ignoreMap":347},[351,674,675],{"class":353,"line":354},[351,676,677],{},"create or replace function public.whoami()\n",[351,679,680],{"class":353,"line":395},[351,681,682],{},"returns json\n",[351,684,685],{"class":353,"line":420},[351,686,687],{},"language sql\n",[351,689,690],{"class":353,"line":439},[351,691,692],{},"security invoker\n",[351,694,695],{"class":353,"line":459},[351,696,697],{},"as $$\n",[351,699,700],{"class":353,"line":538},[351,701,702],{},"  select json_build_object(\n",[351,704,705],{"class":353,"line":545},[351,706,707],{},"    'uid', auth.uid(),\n",[351,709,710],{"class":353,"line":561},[351,711,712],{},"    'role', auth.role(),\n",[351,714,716],{"class":353,"line":715},9,[351,717,718],{},"    'claims', current_setting('request.jwt.claims', true)\n",[351,720,722],{"class":353,"line":721},10,[351,723,724],{},"  );\n",[351,726,728],{"class":353,"line":727},11,[351,729,730],{},"$$;\n",[342,732,734],{"className":344,"code":733,"language":346,"meta":347,"style":347},"const { data } = await supabase.rpc('whoami')\nconsole.log(data)\n",[14,735,736,764],{"__ignoreMap":347},[351,737,738,740,742,744,747,749,751,754,757,759,762],{"class":353,"line":354},[351,739,358],{"class":357},[351,741,362],{"class":361},[351,743,366],{"class":372},[351,745,746],{"class":361}," } ",[351,748,379],{"class":357},[351,750,382],{"class":357},[351,752,753],{"class":361}," supabase.",[351,755,756],{"class":388},"rpc",[351,758,429],{"class":361},[351,760,761],{"class":432},"'whoami'",[351,763,436],{"class":361},[351,765,766,768,770],{"class":353,"line":395},[351,767,548],{"class":361},[351,769,551],{"class":388},[351,771,772],{"class":361},"(data)\n",[10,774,775,776,779,780,783],{},"If ",[14,777,778],{},"uid"," is null and ",[14,781,782],{},"claims"," is null while your token decoded cleanly in step 1, the Data API never verified the token and treated the request as anonymous. No policy rewrite will fix that, which is exactly why people give up and reach for the RLS switch.",[785,786,787],"warning-box",{},[10,788,789,790,793],{},"Don't \"fix\" this by rewriting policies to be more permissive, either. A policy of ",[14,791,792],{},"USING (true)"," while you debug is the same disclosure as disabling RLS, with the added downside that it looks deliberate to anyone reading the schema later and is far easier to forget.",[51,795,797],{"id":796},"rotate-safely-when-you-choose-to","Rotate Safely, When You Choose To",[10,799,800,801,805],{},"If you do want asymmetric keys, the sequence and the waiting are the whole job. From the ",[59,802,804],{"href":182,"rel":803},[63],"signing keys documentation",":",[236,807,808,814,820],{},[239,809,810,811,478],{},"The JWKS discovery endpoint is cached by Supabase's edge servers for ",[231,812,813],{},"10 minutes",[239,815,816,817,478],{},"Supabase client libraries may cache keys in memory for ",[231,818,819],{},"another 10 minutes",[239,821,822],{},"The multi-level cache \"is cleared every 20 minutes, or longer if you have a custom setup.\"",[10,824,825],{},"So a freshly rotated key can be genuinely correct and still be rejected by something that hasn't caught up. Rotating twice in quick succession, which is what a panicking person does, makes that worse rather than better.",[10,827,828],{},"The revoke step has its own rule, quoted directly:",[70,830,831],{},[10,832,833],{},"If your access token expiry time is configured to be 1 hour, wait at least 1 hour and 15 minutes before revoking the legacy JWT secret.",[10,835,836],{},"Revoking early invalidates tokens that are still in real users' browsers. They get logged out, some of them retry, and now you're debugging two problems at once.",[10,838,839,840,843,844,846],{},"A sane order looks like this. Rotate on a staging project first and leave it for a day. On production, rotate during low traffic, then immediately run the step 3 ",[14,841,842],{},"whoami"," check with a real user's token before you touch anything else. If ",[14,845,778],{}," comes back null, roll back to the legacy key while it's still available rather than pushing forward. Only revoke the legacy secret after the expiry window has fully passed and you've confirmed real logins work.",[848,849,850],"tip-box",{},[10,851,852],{},"Keep the legacy secret in the \"Previously used\" state longer than you think you need to. It's your rollback. Once it's revoked, the only way out of a bad rotation is forward, and that's a poor position at 11pm.",[51,854,856],{"id":855},"if-you-already-disabled-rls","If You Already Disabled RLS",[10,858,859],{},"Fix that first, before you resume debugging the key.",[342,861,863],{"className":668,"code":862,"language":670,"meta":347,"style":347},"-- Find every table in your public schema without RLS enabled\nselect tablename\nfrom pg_tables\nwhere schemaname = 'public'\n  and tablename not in (\n    select tablename from pg_tables t\n    join pg_class c on c.relname = t.tablename\n    where c.relrowsecurity = true and t.schemaname = 'public'\n  );\n",[14,864,865,870,875,880,885,890,895,900,905],{"__ignoreMap":347},[351,866,867],{"class":353,"line":354},[351,868,869],{},"-- Find every table in your public schema without RLS enabled\n",[351,871,872],{"class":353,"line":395},[351,873,874],{},"select tablename\n",[351,876,877],{"class":353,"line":420},[351,878,879],{},"from pg_tables\n",[351,881,882],{"class":353,"line":439},[351,883,884],{},"where schemaname = 'public'\n",[351,886,887],{"class":353,"line":459},[351,888,889],{},"  and tablename not in (\n",[351,891,892],{"class":353,"line":538},[351,893,894],{},"    select tablename from pg_tables t\n",[351,896,897],{"class":353,"line":545},[351,898,899],{},"    join pg_class c on c.relname = t.tablename\n",[351,901,902],{"class":353,"line":561},[351,903,904],{},"    where c.relrowsecurity = true and t.schemaname = 'public'\n",[351,906,907],{"class":353,"line":715},[351,908,724],{},[10,910,911],{},"Re-enable it, confirm the policies you had are still there (disabling RLS doesn't drop policies, so they should be intact), and treat any data that was reachable in the meantime as potentially read. How long the window was open matters more than how obscure your project URL is. Your project ref and publishable key are in your frontend bundle, which means they're in anyone's browser cache and possibly in a search engine's.",[913,914,915,940,951,957,969],"faq-section",{},[916,917,919],"faq-item",{"question":918},"Do I have to rotate my Supabase JWT secret?",[10,920,921,922,926,927,931,932,45,934,936,937,478],{},"No. The ",[59,923,925],{"href":197,"rel":924},[63],"announcement"," says plainly: \"You're not required to change your JWT secret unless you choose to.\" The ",[59,928,930],{"href":182,"rel":929},[63],"signing keys docs"," add that if you're not ready to switch away from the legacy JWT secret, \"you can stop here without any issue.\" The late-2026 deadline you may be thinking of is for the ",[14,933,44],{},[14,935,48],{}," API keys, tracked separately in ",[59,938,191],{"href":189,"rel":939},[63],[916,941,943],{"question":942},"Why does auth.uid() return null when my user is definitely logged in?",[10,944,945,947,948,950],{},[14,946,40],{}," reads the JWT claims the Data API handed down to Postgres, not your client's session. If the Data API couldn't verify the token's signature, it treats the request as anonymous rather than erroring, so ",[14,949,40],{}," is null and every policy keyed on it evaluates false. Your client still shows a valid session and GoTrue can still return 200 for the same token, which is why this looks like an RLS bug when it starts as a verification problem.",[916,952,954],{"question":953},"Should I disable RLS to get my app working again?",[10,955,956],{},"No, and the fact that it works is the trap. Turning RLS off makes the blocked writes succeed and at the same moment makes every row in that table readable by anyone holding your publishable key, which ships in your frontend by design. That converts a broken-login incident into a public-database one. If you've already done it, re-enable RLS and roll back the key change instead.",[916,958,960],{"question":959},"What is error 42501 in Supabase?",[10,961,962,964,965,968],{},[14,963,28],{}," is the Postgres code for ",[14,966,967],{},"insufficient_privilege",". Through the Data API it usually surfaces as \"new row violates row-level security policy\", meaning a policy evaluated to false for your write. It doesn't say why. A missing policy and an unverified token that made the request anonymous produce the identical code, which is what makes this failure so easy to misread.",[916,970,972],{"question":971},"How long should I wait after rotating Supabase signing keys?",[10,973,974],{},"The docs give three windows: the JWKS discovery endpoint is cached at the edge for 10 minutes, client libraries may cache keys for another 10, and the multi-level cache clears every 20 minutes or longer on a custom setup. Before revoking the legacy secret, wait at least your access token expiry plus 15 minutes. A 1 hour expiry means waiting 1 hour and 15 minutes.",[976,977,978,983,988],"related-articles",{},[979,980],"related-card",{"description":981,"href":214,"title":982},"The other Supabase migration, the one that does have a deadline. How to move, and how to check your bundle first.","Supabase Is Retiring service_role Keys",[979,984],{"description":985,"href":986,"title":987},"What RLS actually does, and why it's the only thing protecting a key that ships in your frontend.","/blog/how-to/setup-supabase-rls","How to Set Up Supabase Row Level Security",[979,989],{"description":990,"href":991,"title":992},"Verify your policies block what you think they block, before a key rotation makes you find out the hard way.","/blog/how-to/test-supabase-rls","How to Test Supabase RLS Policies",[994,995,997],"cta-box",{"href":497,"label":996},"Start Free Scan",[10,998,999],{},"Scan your deployed app for Supabase tables readable with your publishable key, missing RLS, and admin keys in your JavaScript bundle. Free, no signup required.",[1001,1002,1003],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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":347,"searchDepth":395,"depth":395,"links":1005},[1006,1007,1008,1009,1010,1011],{"id":53,"depth":395,"text":54},{"id":99,"depth":395,"text":100},{"id":222,"depth":395,"text":223},{"id":321,"depth":395,"text":322},{"id":796,"depth":395,"text":797},{"id":855,"depth":395,"text":856},"how-to","2026-08-12","Logged-in users hit RLS error 42501 after migrating to asymmetric JWT signing keys. Why disabling RLS is the wrong fix, and why you may not have needed to rotate at all.",false,"md",[1018,1020,1022,1024,1026],{"question":918,"answer":1019},"No. Supabase's announcement says plainly: 'You're not required to change your JWT secret unless you choose to.' The signing keys docs add that if you're not ready to switch away from the legacy JWT secret, 'you can stop here without any issue.' The late-2026 deadline people are reacting to is for the anon and service_role API keys, which is a different migration with a different tracking discussion.",{"question":942,"answer":1021},"Because auth.uid() reads the JWT claims that the Data API passed down to Postgres, not your client's session. If the Data API could not verify the token's signature, it falls back to treating the request as anonymous instead of erroring, so auth.uid() is null and every policy keyed on it fails. Your client still shows a valid session, and GoTrue's /auth/v1/user endpoint can still return 200 for the same token, which is why this reads as an RLS bug when it starts as a verification problem.",{"question":953,"answer":1023},"No. It works, and that's the trap. Turning RLS off on a table makes the blocked writes succeed and at the same moment makes every row in that table readable by anyone who has your publishable key, which by design ships in your frontend. You would be converting a broken-login incident into a public-database incident. If you already did this to restore service, re-enable RLS now and roll back the key change instead.",{"question":959,"answer":1025},"42501 is the Postgres error code for insufficient_privilege. Through the Supabase Data API it usually surfaces as 'new row violates row-level security policy', meaning a policy evaluated to false for your INSERT. It does not tell you why. A missing policy and an unverified token that made the request anonymous produce the same code.",{"question":1027,"answer":1028},"How long do I have to wait after rotating Supabase signing keys?","The docs give three windows. The JWKS discovery endpoint is cached by Supabase's edge servers for 10 minutes, client libraries may cache keys in memory for another 10 minutes, and the multi-level cache is cleared every 20 minutes or longer on a custom setup. Before revoking the legacy secret, wait at least your access token expiry plus 15 minutes, so a 1 hour expiry means waiting 1 hour and 15 minutes.","yellow",null,"supabase auth.uid null, supabase jwt signing keys, rls 42501 after key rotation, supabase es256 migration, supabase asymmetric jwt, supabase rls blocking authenticated users",{"trendTrigger":1033,"trendDate":1034},"supabase-asymmetric-jwt-signing-key-migration-reports-authenticated-requests-resolving-as-anon","2026-07-27","Your user is logged in, GoTrue says the token is valid, and every insert fails with 42501. Here's the diagnosis path, and the one workaround that turns it into a public database.","/blog/how-to/supabase-auth-uid-null-after-key-rotation","9 min read","[object Object]","HowTo",{"title":5,"description":1014},{"loc":1036},"blog/how-to/supabase-auth-uid-null-after-key-rotation",[],"summary_large_image","lwi-QYcCd7668b6zzCmSjB8BIFIVjeGAGYi4wdlm-6k",1787602618924]