Your user is logged in. The session is valid, the access token is in the Authorization header, and supabase.auth.getUser() cheerfully returns their profile. Then every insert fails with new row violates row-level security policy, Postgres error 42501, and updates quietly change zero rows without raising anything at all.
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.
TL;DR
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 anon and service_role API keys, which is a different migration.
The Four-Second Fix That Publishes Your Database
Here is the exact sequence, taken from a developer in Supabase discussion #45812 on 2026-05-12. They rotated from the legacy HS256 secret to ECC P-256, authenticated inserts started failing with 42501, and they tried the obvious thing:
Disabling RLS on the table allows inserts to succeed.
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 anon 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.
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.
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 users table is a disclosure.
First, Check Whether You Needed to Rotate at All
This is the part that gets skipped, and it's the one that would have prevented most of these incidents.
Supabase is running two separate migrations, and people are conflating them:
| Legacy JWT secret to asymmetric signing keys | anon / service_role to sb_publishable_ / sb_secret_ | |
|---|---|---|
| Mandatory? | No | Yes |
| Deadline | None announced | Late 2026, marked TBC |
| What it changes | How tokens are signed and verified | Which API key string your app sends |
| Tracking | Signing keys docs | discussions/29260 |
The announcement post is unambiguous about the first one:
You're not required to change your JWT secret unless you choose to.
And the signing keys documentation says that if you aren't ready, "you can stop here without any issue."
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 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.
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.
What the Reports Actually Say
Being precise here matters, because the honest version of this story is narrower than the alarming version.
Since May 2026, at least three distinct GitHub accounts have filed reports describing the same shape of failure on hosted Supabase projects after migrating to asymmetric signing keys:
- 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.
- Issue #48341 (2026-07-27) reports
auth.uid()returning null despite a valid session, withauth.uid()working correctly when the claims are supplied manually. - 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.
Now the caveats, because they change what you should conclude.
All three issues were closed on the same day they were opened, and all three still carry a 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.
One nearby case did get a maintainer response, and it's a different symptom. In discussion #48246 (2026-07-23), five people reported intermittent 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.
Most recently, 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.
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 when you rotate. It isn't enough to tell you the platform is broken, and this post won't claim that.
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 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.
Diagnose It Before You Change Anything
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.
Read the token your client is actually sending.
Not the session object your app displays. The raw access token in the Authorization header. Grab it and decode the header and payload:
const { data: { session } } = await supabase.auth.getSession()
const [header, payload] = session.access_token
.split('.')
.slice(0, 2)
.map(part => JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/'))))
console.log(header) // { alg: "ES256", kid: "...", typ: "JWT" }
console.log(payload) // { sub: "...", role: "authenticated", exp: ... }
Two things to check. alg should match your currently active signing key, and role should be authenticated. If role says anon, your user isn't logged in the way you think and this is a client-side session problem, not a key problem.
Confirm the token's kid is in the published JWKS.
Your project publishes its public keys. Fetch them and look for the kid you just read:
curl -s "https://<PROJECT_REF>.supabase.co/auth/v1/.well-known/jwks.json" \
| grep -o '"kid":"[^"]*"'
If the token's kid isn't in that list, the issuer and the verifier disagree about which key is current. That's the 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.
Ask the database what it sees.
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:
create or replace function public.whoami()
returns json
language sql
security invoker
as $$
select json_build_object(
'uid', auth.uid(),
'role', auth.role(),
'claims', current_setting('request.jwt.claims', true)
);
$$;
const { data } = await supabase.rpc('whoami')
console.log(data)
If uid is null and 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.
Don't "fix" this by rewriting policies to be more permissive, either. A policy of 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.
Rotate Safely, When You Choose To
If you do want asymmetric keys, the sequence and the waiting are the whole job. From the signing keys documentation:
- The JWKS discovery endpoint is cached by Supabase's edge servers for 10 minutes.
- Supabase client libraries may cache keys in memory for another 10 minutes.
- The multi-level cache "is cleared every 20 minutes, or longer if you have a custom setup."
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.
The revoke step has its own rule, quoted directly:
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.
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.
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 whoami check with a real user's token before you touch anything else. If uid 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.
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.
If You Already Disabled RLS
Fix that first, before you resume debugging the key.
-- Find every table in your public schema without RLS enabled
select tablename
from pg_tables
where schemaname = 'public'
and tablename not in (
select tablename from pg_tables t
join pg_class c on c.relname = t.tablename
where c.relrowsecurity = true and t.schemaname = 'public'
);
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.
Do I have to rotate my Supabase JWT secret?
No. The 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 you may be thinking of is for the anon and service_role API keys, tracked separately in discussions/29260.
Why does auth.uid() return null when my user is definitely logged in?
auth.uid() 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 auth.uid() 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.
Should I disable RLS to get my app working again?
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.
What is error 42501 in Supabase?
42501 is the Postgres code for 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.
How long should I wait after rotating Supabase signing keys?
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.
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.