Asymmetric JWT Authentication for Self-Hosted Supabase: ES256 Migration Guide

Learn how to migrate your self-hosted Supabase from HS256 to ES256 asymmetric JWTs for better security and easier token verification.

Cover Image for Asymmetric JWT Authentication for Self-Hosted Supabase: ES256 Migration Guide

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:

AlgorithmTypeNotes
RS256RSAWidely supported, larger keys
ES256ECCDefault, faster, smaller keys
EdDSAEd25519Most performant, limited support
HS256SymmetricLegacy, 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:

  1. Check algorithm mismatch: Ensure all services have the updated JWKS configuration
  2. Verify key format: The private key must be in PEM format with proper newlines
  3. 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:

  1. Restart all affected containers
  2. Wait for cache expiry (typically 20-60 minutes)
  3. 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:

  1. Add a new key pair to JWT_KEYS and JWT_JWKS
  2. Wait for the old tokens to expire
  3. 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.


Further Reading