name: supabase-cloud-storage-jwt-debug
description: Debug JWT signature failures on Supabase Cloud Storage when REST API works but Storage API returns InvalidKey or signature verification failed
category: supabase
Supabase Cloud Storage JWT Debug Skill
Context
When Supabase Cloud Storage gives "InvalidKey" or "signature verification failed" on JWT validation but REST API works fine.
Symptoms
- -
GET /storage/v1/bucketreturns{"statusCode":"400","error":"InvalidKey","message":"Invalid key: "} - -
GET /storage/v1/bucketreturns{"statusCode":"403","error":"Unauthorized","message":"signature verification failed"} - - REST API (
/rest/v1/...) works fine with anon key - - JWT secret has been rotated in Supabase Dashboard
- - ✅ Work for REST (no JWT verification)
- - ❌ Fail for Storage (JWT verification required)
- -
POST /v1/projects/{ref}/api-keys/anon/rotate→ 404 not found - -
POST /v1/projects/{ref}/api-keys/rotate→ 404 not found - - Only works for newer key types, not legacy
anon/service_role - - Files stored on VPS at
/var/www/documentos/uploads/{user_id}/{client_id}/{filename} - - Served statically via Nginx at
https://documentos.rochasalesseguros.com.br/uploads/ - - Upload/delete handled by TanStack Start server routes (no Edge Functions needed)
- - No Supabase Storage used — eliminates JWT dependency entirely
- - Change
supabase.storage.from('documents').upload→POST /api/public/upload-document - - Change
supabase.storage.from('documents').remove→POST /api/public/delete-document - - Pass
transport: { WebSocket }to Supabase client to avoid Node.js 20 WebSocket errors - - DocCorretor + ComercialRS share the SAME Supabase project (
dauftiqcvgaydddoxqhh) — same.env, same keys, same storage - - Changing Supabase keys affects BOTH projects
- -
systemctl restart documentosto restart the server after code changes - - Supabase Cloud (shared instance) — storage validates JWT
- - Supabase Self-hosted (Docker on VPS) — Kong handles auth differently
- - Project
dauftiqcvgaydddoxqhh— Dashboard Rocha Sales Meta e CRM - - Used by: DocCorretor (VPS) + ComercialRS (VPS) — SAME keys, SAME project
- - Bucket
documentscreated 2026-05-14T18:17:31, public=true
Key Findings
Supabase uses TWO different auth systems
1. REST API (/rest/v1/) — validates against project's database directly, NOT JWT signature
2. Storage API (/storage/v1/) — validates JWT signature using project's current JWT secret
If JWT secret was rotated, keys signed with the OLD secret will:
Management API (PAT) cannot rotate legacy keys
Supabase CLI link requires DATABASE password, not PAT
`bash
cd app && npx supabase link --project-ref dauftiqcvgaydddoxqhh -p
This asks for DATABASE password (from Supabase Dashboard → Settings → Database)
NOT the PAT or account password
`
Bucket existence check (even with InvalidKey error)
`bash
Try listing objects — if bucket exists, you'll get files or empty array
curl -s "https://{REF}.supabase.co/storage/v1/object/documents/" \
-H "Authorization: Bearer $ANON_KEY"
`
JWKS endpoint reveals key type
`bash
curl -s "https://{REF}.supabase.co/auth/v1/.well-known/jwks.json"
If returns EC keys (ES256) → project uses asymmetric keys
If returns HS256 keys → uses symmetric HMAC (the JWT secret)
`
Verifying JWT signature locally
`node
node -e "
const crypto = require('crypto');
const parts = token.split('.');
const signingInput = parts.slice(0, 2).join('.');
const expectedSig = parts[2];
const computed = crypto.createHmac('sha256', Buffer.from(JWT_SECRET, 'base64'))
.update(signingInput).digest('base64').replace(/=/g, '');
console.log(computed === expectedSig.replace(/=/g, ''));
"
`
Solution Path
Option A: Rotate keys in Dashboard (if accessible)
1. Go to Settings → API in Supabase Dashboard
2. Click Regenerate next to service_role key (anon key can stay)
3. Update SUPABASE_SERVICE_ROLE_KEY in .env
4. For DocCorretor + ComercialRS (same project), update both .env files
5. Test storage: curl -s "https://{REF}.supabase.co/storage/v1/bucket" -H "Authorization: Bearer $SERVICE_KEY"
Option B: Local VPS storage (preferred when Dashboard is inaccessible)
When the Supabase Dashboard is not reachable (browser anti-bot, account issues) or key rotation is unavailable via API, migrate to local VPS storage.
#### Architecture
#### Nginx config (add to server block)
`nginx
location /uploads/ {
alias /var/www/documentos/uploads/;
expires 30d;
autoindex off;
client_max_body_size 100M;
}
`
#### TanStack Start server route (upload handler)
app/src/routes/api/public/upload-document.ts — handles file upload, auth, type/size validation, local FS write. Auth via Authorization: Bearer {token} header.
#### TanStack Start server route (delete handler)
app/src/routes/api/public/delete-document.ts — handles file deletion from local storage.
#### Client-side changes (clients.$id.tsx)
#### Node.js 20 + Supabase WebSocket issue
Supabase's auth.getUser() in Node.js 20 throws:
`
Node.js 20 detected without native WebSocket support.
Suggested solution: install "ws" package and provide it via transport option.
`
Even with ws installed and transport: { WebSocket }, this error can persist in server routes.
Solution: Validate JWT directly using jsonwebtoken instead of supabase.auth.getUser():
`node
import jwt from 'jsonwebtoken';
const { aud, exp } = jwt.verify(token, Buffer.from(JWT_SECRET, 'base64'), { algorithms: ['HS256'] });
`
#### Required packages
`bash
npm install ws dotenv jsonwebtoken
`
#### Critical constraints