name: supabase-anon-key-invalid-debug
description: Debug and fix "Invalid API key" 401 errors when Supabase anon key is stale/regenerated
Supabase Anon Key Invalid — Debug & Fix
Symptom
Supabase REST API returns {"message":"Invalid API key"} in the browser — ALL tables fail with 401, even with RLS disabled. Edge Functions (using service role) work fine.
Root Cause
Supabase regenerates anon keys when: Lovable regenerates the project, Supabase project is recreated, or certain dashboard operations are performed. The anon key stored in the VPS .env becomes stale.
Diagnostic Steps (in order)
1. Check if RLS is the issue — temporarily disable RLS on affected tables:
`sql
ALTER TABLE profiles DISABLE ROW LEVEL SECURITY;
ALTER TABLE user_roles DISABLE ROW LEVEL SECURITY;
`
If errors persist after RLS is off → the problem is the API key itself.
2. Check Edge Functions — if Edge Functions that use createClient(SUPABASE_URL, SERVICE_KEY) work but browser REST calls fail → confirms anon key is stale, not RLS.
3. Check JWT timestamp — decode the anon key at https://jwt.io and look at iat (issued at):
- If iat is older than ~Nov 2024 and the app stopped working around then → key was regenerated.
- Known stale key had iat: 1772451385 (2025-11-04).
4. Try to recover from Edge Function env — the service role key is stored in Edge Function secrets and includes the anon key. Check if SUPABASE_ANON_KEY is in Edge Function environment config.
Fix (2-minute update on VPS)
1. Get fresh anon key from Supabase dashboard:
- https://supabase.com/dashboard → project → Settings → API
- Copy "anon public" key (starts with eyJhbGci...)
2. Update on VPS:
`bash
nano /path/to/app/.env
# Find: VITE_SUPABASE_PUBLISHABLE_KEY=eyJhbGci...
# Or: SUPABASE_ANON_KEY=eyJhbGci...
# Replace with fresh key
`
3. Restart the Node server:
`bash
fuser -k 3010/tcp
cd /path/to/app
PORT=3010 node server.js &>/tmp/doc.log &
`
4. Clear browser cache and test login.
Prevention
- - When Lovable regenerates a project, immediately export the new anon key and update VPS .env
- - The service role key does NOT change — only the anon key gets regenerated
- - Edge Functions continue working because they use service role stored in secrets
- - If Dashboard → Settings → Authentication → JWT Settings → "Regenerate JWT secret" is clicked: ALL existing API keys (anon + service_role) become invalid. Must regenerate both keys via Settings → API.
- - Root cause: the JWT Secret was regenerated in Supabase Dashboard (Settings → Authentication → JWT Settings), but the API keys in .env were signed with the OLD JWT secret.
- - API keys (both anon and service_role) are signed with the JWT secret at the time they were generated. If the JWT secret changes, existing keys become invalid for operations that do JWT validation (Storage, Auth).
- - REST API vs Storage API behavior: REST API (PostgREST) may accept tokens even with wrong JWT secret (doesn't always do local HMAC validation), but Storage API strictly validates JWT signatures and fails with "Invalid Compact JWS".
- - Fix: Regenerate BOTH anon key AND service_role key via Dashboard → Settings → API → click "Regenerate" (do NOT just copy the existing key field — that gives you the same stale key). The new keys will be signed with the current JWT secret.
- - Diagnostic: Decode JWT at https://jwt.io — if
iat(issued at) is old and doesn't match when you test locally with the current JWT secret, the keys are stale. - - Local verification: Use Node.js to verify JWT locally:
- - After regenerating keys: update .env with new
VITE_SUPABASE_ANON_KEYandSUPABASE_SERVICE_ROLE_KEY, then rebuild and restart. - - The
Authorization: Bearerheader carries the user's JWT from the browser session - - The
apikeyheader must carry the anon key (public key), NOT the service role key - - Using service role key as
apikeywith a user token →{"message":"Invalid API key"}from Supabase gateway - - Using anon key as
apikeywith a user token → works, returns user data - - Env var naming: the anon key is stored in
.envasVITE_SUPABASE_PUBLISHABLE_KEY(notSUPABASE_ANON_KEY) - - The
SUPABASE_SERVICE_ROLE_KEYis only for server-to-server calls that need elevated privileges - - The endpoint validates the token signature but rejects it because it's not a user auth token
- - Response:
{"code":403,"error_code":"bad_jwt","msg":"invalid claim: missing sub claim"} - - This is expected —
/auth/v1/useronly works with real user access tokens, not anon keys - - Project:
dauftiqcvgaydddoxqhh(West US) - - PAT: stored in hermes memory
- - Auth URL:
https://dauftiqcvgaydddoxqhh.supabase.co
Key Diagnostic Patterns Learned
Pattern: JWT Secret mismatch (Invalid Compact JWS on Storage)
When Storage API returns {"statusCode":"400","error":"Invalid Compact JWS"} but REST API works:
`js
const crypto = require('crypto');
const secret = Buffer.from('YOUR_JWT_SECRET_BASE64', 'base64');
// Sign the header.payload with the secret and compare to the signature part
`
Pattern: Auth token from browser (localStorage) vs service role key confusion
When calling Supabase Auth REST API directly from server-side code (e.g. upload-document endpoint):
Pattern: /auth/v1/user with anon token (role=anon) returns 403 "missing sub claim"
When you call /auth/v1/user with an anon-scoped token (no sub in JWT payload):
When REST API returns {"message":"Invalid API key"} for ALL endpoints (including simple GET) but Edge Functions with service role key succeed:
1. Supabase CLI db query --linked still works → use it to query DB directly as workaround
2. Dashboard at supabase.com may also fail to load API page (same underlying auth issue)
3. Root cause may be PostgREST gateway or API key infrastructure issue, not just stale key
4. Fix: regenerate anon key via dashboard URL or contact Supabase support if dashboard is also inaccessible
Pattern: Google OAuth "This browser or app may not be secure"
Browser automation gets blocked by Google's anti-bot check when using "Entrar com Google" in an automated browser context. This is NOT an app bug — it's expected behavior. Workaround:
1. User must test OAuth flow manually in their own browser
2. Verify auth success via: (a) browser console network tab, (b) Supabase CLI direct DB query for profile/roles
3. For automated testing: use Supabase CLI db query --linked to verify profile exists and has correct roles after user logs in
PAT-based diagnostic (BEST method)
Get a Personal Access Token from https://supabase.com/dashboard/account/tokens and use the management API:
`bash
PAT="sbp_..."
List projects
curl -s "https://api.supabase.com/v1/projects" -H "Authorization: Bearer $PAT"
List API keys
curl -s "https://api.supabase.com/v1/projects/{ref}/api-keys" -H "Authorization: Bearer $PAT"
List storage buckets
curl -s "https://api.supabase.com/v1/projects/{ref}/storage/buckets" -H "Authorization: Bearer $PAT"
List secrets
supabase secrets list --project-ref {ref}
`
PAT is stored in hermes memory (search "sbp_").
Supabase CLI multi-project awareness
Use --project-ref flag to target correct project (CLI defaults to linked project which may be wrong):
`bash
npx supabase link --project-ref dauftiqcvgaydddoxqhh
npx supabase projects list # shows all projects and their refs
`
Two projects share same Supabase Cloud project
Both DocCorretor and ComercialRS use project dauftiqcvgaydddoxqhh with identical keys. Storage fix benefits both apps simultaneously.
Pattern: "Invalid API key" on ALL endpoints but project is healthy (key regenerated)
Symptom: REST API returns {"message":"Invalid API key"} for ALL endpoints including /auth/v1/health. Headers show sb-project-ref in response, confirming the project IS receiving requests. Project status in management API shows ACTIVE_HEALTHY. Edge Functions may still work.
Root cause: Anon key was regenerated in Supabase Dashboard. The key stored in .env is the old one.
Diagnostic: Management API with project API key works fine for listing projects:
`bash
curl -s "https://api.supabase.com/v1/projects" \
-H "Authorization: Bearer PROJECT_API_KEY"
Returns: [{"id": "dauftiqcvgaydddoxqhh", "name": "Dashboard Rocha Sales Meta e CRM", "status": "ACTIVE_HEALTHY"}]
`
JWT decode of stale key shows exp: 1810636724 (2027) — seems valid but was still regenerated. The project ref is correct and healthy.
Fix: Get fresh anon key from Dashboard → Settings → API → "anon public" key, or regenerate and update .env.
Note: The token sbp_REDACTED is a project API key (starts with sbp_), NOT a Personal Access Token (which starts with sbp_1d0b...). Management API for user management needs a real PAT from account tokens page.
Supabase Project Reference
New Patterns (2026-05-16)
Pattern: RLS blocks anon key REST reads even with policies in place
Symptom: supabase db query --linked shows data exists, but REST SELECT * FROM table returns [] or empty. The table has RLS policies but SELECT returns empty.
Fix:
`bash
Create explicit RLS policies via CLI (bypasses PostgREST schema cache)
npx supabase db query "CREATE POLICY \"Allow all read\" ON public.YOUR_TABLE FOR SELECT USING (true);" --linked
npx supabase db query "CREATE POLICY \"Allow all insert\" ON public.YOUR_TABLE FOR INSERT WITH CHECK (true);" --linked
npx supabase db query "CREATE POLICY \"Allow all update\" ON public.YOUR_TABLE FOR UPDATE USING (true);" --linked
`
Verified: chatwoot_painel_map table returned [] via REST even with policies — after running CREATE POLICY via --linked, REST immediately returned data.
Pattern: /adm redirects to / even after fixing anon key — empty profiles and user_roles tables
Symptom: After fixing a stale anon key, the /adm route still redirects to / instead of showing the admin panel. The AdmGuard component checks user_roles table for role='admin' AND profiles table for role.
Root cause chain:
1. Stale anon key → REST API returns Invalid API key → can't read/write any tables
2. profiles table is empty → no user profile exists for any user
3. user_roles table is empty → no admin role assignments exist
4. AdmGuard queries user_roles for role='admin' → gets empty result → redirects to /
5. AuthContext queries profiles.role → gets empty result → defaults to 'pre_sales'
Diagnosis:
`bash
Check if profiles table has data (use anon key after refresh)
curl -s 'https://PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1' \
-H 'apikey: ANON_KEY'
Check user_roles table
curl -s 'https://PROJECT.supabase.co/rest/v1/user_roles?select=*' \
-H 'apikey: ANON_KEY'
`
Fix requires TWO steps after getting a fresh anon key:
1. Find the user's auth.uid from auth.users (may need Supabase Dashboard → Authentication → Users, or use management API)
2. Insert into profiles (if schema uses profiles.role) and user_roles
Note: psql direct access on VPS does NOT work — Supabase Cloud is a managed service, not self-hosted postgres on the VPS. Management API with a real PAT is needed to query auth.users.
Known user_roles schema for ComercialRS:
`sql
-- structure: id, user_id (uuid), role (text), team_member_id, name, email, phone, active
INSERT INTO user_roles (user_id, role, name, email, active)
VALUES ('USER_UUID_HERE', 'admin', 'Tecrocha Sales', 'tecrochasales@gmail.com', true);
`
Known profiles schema: profiles.user_id → uuid FK to auth.users, profiles.role → text (admin|manager|pre_sales)
Pattern: Edge Function returns UNAUTHORIZED_NO_AUTH_HEADER with valid anon key
Edge Functions require BOTH headers — using only apikey fails with UNAUTHORIZED_NO_AUTH_HEADER. Correct pattern:
`bash
curl -X POST "https://PROJECT.supabase.co/functions/v1/FUNCTION_NAME" \
-H "Content-Type: application/json" \
-H "apikey: ANON_KEY" \
-H "Authorization: Bearer ANON_KEY" \
-d '{}'
`
Same anon key works for both header values. This is needed for every Edge Function call, not just authenticated ones.
Pattern: Edge Function uses service_role key internally for DB writes
Inside Edge Functions, use Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') with createClient(supabaseUrl, serviceRoleKey) — this bypasses RLS and allows INSERT/UPDATE on any table. The anon key cannot INSERT via REST even with policies — the service role is required.