If you've recently upgraded your self-hosted Supabase installation, you may have encountered unexpected 401 errors on authenticated requests. The culprit? Supabase's transition from symmetric HS256 JWT signing to asymmetric ES256 keys—a change that improves security but requires careful configuration for self-hosters.
This guide walks you through understanding asymmetric JWTs, configuring them for self-hosted Supabase, and migrating without breaking your existing user sessions.
Why Supabase Moved to Asymmetric JWTs
For years, Supabase used symmetric HS256 signing with a shared JWT_SECRET. While simple, this approach had significant drawbacks:
Security risks with shared secrets: Every service that needs to verify tokens must know the secret. If one service is compromised, the entire authentication system is at risk.
Rotation complexity: Changing the JWT secret requires coordinated updates across all services simultaneously, often causing downtime.
No public verification: Third-party services couldn't verify tokens without access to your secret, limiting integration possibilities.
Asymmetric keys solve these problems. With ES256 (Elliptic Curve Digital Signature Algorithm), only the Auth service holds the private key for signing. All other services—PostgREST, Realtime, Storage, and even external integrations—verify tokens using the public key.
Understanding the New Key Architecture
The asymmetric JWT system introduces several new components:
Key Pairs
- Private Key: Held only by Supabase Auth, used to sign new JWTs
- Public Key: Distributed to all services for verification via JWKS
JWKS (JSON Web Key Set)
Your public keys are exposed at a well-known endpoint:
https://your-supabase-url/auth/v1/.well-known/jwks.json
Services fetch this endpoint to get the current public keys for verification. The JWKS can contain multiple keys, enabling zero-downtime rotation.
Algorithm Options
Supabase supports several signing algorithms:
| Algorithm | Type | Notes |
|---|---|---|
| RS256 | RSA | Widely supported, larger keys |
| ES256 | ECC | Default, faster, smaller keys |
| EdDSA | Ed25519 | Most performant, limited support |
| HS256 | Symmetric | Legacy, still supported for migration |
ES256 is the recommended default—it offers the best balance of security, performance, and compatibility.
Configuring Asymmetric JWTs for Self-Hosted
Before starting, ensure you're running Supabase CLI v2.71.1+ or have updated your Docker Compose configuration to the latest images.
Step 1: Generate Your Key Pair
Create an ES256 key pair. You can use OpenSSL or a helper script:
# Generate EC private key openssl ecparam -genkey -name prime256v1 -noout -out private.pem # Extract public key openssl ec -in private.pem -pubout -out public.pem # Convert to base64 for environment variables cat private.pem | base64 -w 0 > private.b64 cat public.pem | base64 -w 0 > public.b64
Step 2: Configure Environment Variables
Update your .env file with the new JWT configuration:
# New asymmetric JWT configuration
JWT_KEYS='[
{
"alg": "ES256",
"key_id": "your-key-id-1",
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----"
}
]'
# JWKS containing public key(s) for verification
JWT_JWKS='{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "your-key-id-1",
"x": "...",
"y": "..."
}
]
}'
# Keep legacy secret for backward compatibility during migration
JWT_SECRET=your-existing-jwt-secret
The JWT_JWKS should contain both your new EC public key and the legacy symmetric key (as a JWK) to verify existing tokens during migration.
Step 3: Update Service Configurations
Each service that verifies JWTs needs access to the JWKS:
PostgREST (in docker-compose.yml):
rest:
environment:
- PGRST_JWT_SECRET=${JWT_JWKS}
Realtime:
realtime:
environment:
- JWT_JWKS=${JWT_JWKS}
Storage:
storage:
environment:
- JWT_JWKS=${JWT_JWKS}
Edge Functions (if using):
functions:
environment:
- VERIFY_JWT=true
- JWT_JWKS=${JWT_JWKS}
For more detailed configuration guidance, check out Supabase's self-hosting documentation.
Step 4: Restart Services
Apply the configuration:
docker compose down docker compose up -d
After restart, verify the JWKS endpoint is accessible:
curl https://your-domain/auth/v1/.well-known/jwks.json
You should see your public keys in JWK format.
Migration Strategy: Zero-Downtime Transition
The biggest challenge is migrating without invalidating existing user sessions. Here's a proven approach:
Phase 1: Enable Dual Verification (Week 1)
Configure your services to accept both HS256 and ES256 tokens by including both keys in your JWKS:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "new-es256-key",
"x": "...",
"y": "..."
},
{
"kty": "oct",
"kid": "legacy-symmetric",
"k": "base64-encoded-jwt-secret"
}
]
}
Phase 2: Switch Signing to ES256 (Week 2)
Once all services can verify both token types, update Auth to sign new tokens with ES256 by setting JWT_KEYS. New sessions will receive ES256 tokens while existing HS256 tokens remain valid.
Phase 3: Remove Legacy Keys (Week 4+)
After your JWT expiry period (default: 1 hour) plus a buffer, all active tokens will be ES256. You can safely remove the legacy symmetric key from your JWKS.
Important timing consideration: Supabase recommends waiting at least 20 minutes after any JWKS change before revoking keys, due to caching. If you have long-lived sessions, extend this window accordingly.
Common Migration Issues and Solutions
401 Errors After Upgrade
If you're suddenly getting unauthorized errors after upgrading:
- Check algorithm mismatch: Ensure all services have the updated JWKS configuration
- Verify key format: The private key must be in PEM format with proper newlines
- Test the JWKS endpoint: Confirm it returns valid JSON
Edge Functions Failing
Edge Functions verify JWTs differently. If you see JWSError: JWSInvalidSignature, ensure:
# In your edge function configuration
VERIFY_JWT=true
JWT_JWKS='{...}' # Same JWKS as other services
Services Not Picking Up New Keys
The JWKS is cached by services. After making changes:
- Restart all affected containers
- Wait for cache expiry (typically 20-60 minutes)
- Clear any CDN or proxy caches in front of your services
Security Benefits of Asymmetric JWTs
The migration effort pays off with significant security improvements:
Reduced Attack Surface
Only your Auth service can sign tokens. Even if an attacker compromises PostgREST or Storage, they can't forge authentication tokens.
Easier Third-Party Integration
External services can verify your JWTs using only the public JWKS endpoint—no shared secrets required. This enables secure integrations with services like custom OAuth providers.
Simpler Key Rotation
Rotate keys without coordinating across services:
- Add a new key pair to
JWT_KEYSandJWT_JWKS - Wait for the old tokens to expire
- Remove the old key
No service restarts required during rotation—just JWKS cache refresh.
Compliance Ready
Many compliance frameworks (SOC 2, HIPAA, PCI-DSS) prefer or require asymmetric cryptography for token signing. This migration helps meet those requirements, which is particularly important for self-hosted deployments handling sensitive data.
Verifying Your Configuration
After migration, validate everything works correctly:
Test Token Verification
# Get a token
TOKEN=$(curl -X POST 'https://your-domain/auth/v1/token?grant_type=password' \
-H 'apikey: your-anon-key' \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"password"}' \
| jq -r '.access_token')
# Decode and verify the algorithm
echo $TOKEN | cut -d. -f1 | base64 -d
# Should show: {"alg":"ES256","typ":"JWT","kid":"your-key-id"}
Test API Access
# Query with the new token curl 'https://your-domain/rest/v1/your_table' \ -H "Authorization: Bearer $TOKEN" \ -H "apikey: your-anon-key"
Check JWKS Caching
Monitor your JWKS endpoint access logs. After initial cache warm-up, you should see periodic refreshes (every ~5 minutes) rather than per-request fetches.
What About the New API Key System?
Supabase is also transitioning from the legacy anon and service_role keys to new sb_publishable_* and sb_secret_* keys. While related, this is a separate change:
- JWT signing: Controls how user session tokens are signed (HS256 → ES256)
- API keys: Controls how applications authenticate to the API gateway
You can adopt asymmetric JWTs now while continuing to use legacy API keys. Both systems will coexist through 2026, giving you time for a phased migration. For API key rotation guidance, see our complete API key management guide.
Performance Considerations
ES256 verification is slightly slower than HS256 (microseconds, not milliseconds), but the practical impact is negligible. The bigger consideration is JWKS caching:
- Enable caching: Services should cache the JWKS, not fetch it per-request
- Set appropriate TTL: 5-15 minutes balances freshness with performance
- Pre-warm on startup: Fetch JWKS during service initialization
For high-throughput deployments, consider placing your JWKS behind a CDN to reduce load on the Auth service.
Conclusion
Migrating to asymmetric JWTs is one of those infrastructure improvements that's easy to defer but pays dividends in security and operational simplicity. The dual-verification approach ensures you can migrate without disrupting users, and the end result is a more secure, more flexible authentication system.
For self-hosters, this migration also future-proofs your deployment as Supabase continues evolving their authentication architecture. The legacy HS256 system works today, but asymmetric keys are the clear direction for the platform.
Ready to simplify your self-hosted Supabase management? Supascale handles configuration complexity like JWT key management through an intuitive UI, letting you focus on building your application instead of wrestling with environment variables.
