Skip to content

Realtime JWKS verification fails with Clerk custom domains (/.well-known/jwks 404 vs /.well-known/jwks.json) #1791

@19Ash82

Description

@19Ash82
  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

Supabase Realtime fails to verify JWTs issued by Clerk custom domains. The WebSocket connects (status 101) but immediately closes after ~350ms with CHANNEL_ERROR.

The root cause: Realtime fetches JWKS from {issuer}/.well-known/jwks, but Clerk custom domains only serve JWKS at /.well-known/jwks.json. The dev domain (*.clerk.accounts.dev) serves both paths, so the issue only appears in production with custom domains.

This same path issue was already fixed for Supabase Auth in supabase/auth#1724, but Realtime was not updated.

To Reproduce

  1. Configure Supabase Third-Party Auth with a Clerk custom domain (e.g., clerk.example.com CNAME to frontend-api.clerk.services)
  2. Use native Clerk session tokens — getToken() returns token with iss: "https://clerk.example.com"
  3. Create a Supabase client and subscribe to a Realtime channel:
    const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    global: { headers: { Authorization: Bearer ${token} } },
    });
    supabase.realtime.setAuth(token);

const channel = supabase.channel('my-channel');
channel.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'my_table', filter: id=eq.${id} }, callback);
channel.subscribe((status) => {
// status === 'CHANNEL_ERROR' — every time
});
4. Channel immediately returns CHANNEL_ERROR, WebSocket closes after ~350ms

Expected behavior

Realtime should verify the JWT successfully using the JWKS available at the OIDC-standard endpoint (/.well-known/jwks.json), as advertised by Clerk's OIDC discovery document.

Screenshots

Evidence of the JWKS path mismatch:

Network tab shows WebSocket connects (101) then closes after 348ms on production, while staying open indefinitely on dev domain (where both JWKS paths work).

System information

  • OS: Linux
  • Browser: Chrome/Edge
  • Version of supabase-js: 2.x (latest)
  • Version of Node.js: 20.x
  • Supabase: Hosted (not self-hosted)
  • Auth provider: Clerk v7 with custom domain + native Supabase third-party integration

Additional context

  • REST API / PostgREST works fine with the same token — only Realtime is affected
  • Clerk dev domains (*.clerk.accounts.dev) serve JWKS at both /.well-known/jwks AND /.well-known/jwks.json, which is why this only breaks in production
  • Workaround: Use a Clerk JWT template (getToken({ template: 'supabase' })) instead of native session tokens for Realtime subscriptions. Template tokens use
    a different JWKS verification path.
  • Suggested fix: Realtime should fetch {issuer}/.well-known/openid-configuration and use the jwks_uri from the OIDC discovery document, rather than
    hardcoding /.well-known/jwks. Alternatively, try /.well-known/jwks.json as a fallback.
  • Related: supabase/auth#1724 — same .well-known/jwks vs .well-known/jwks.json issue, fixed for Supabase Auth but not Realtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions