📄 SKILL.md

← Vault

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

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.