- 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
- Configure Supabase Third-Party Auth with a Clerk custom domain (e.g., clerk.example.com CNAME to frontend-api.clerk.services)
- Use native Clerk session tokens — getToken() returns token with iss: "https://clerk.example.com"
- 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.
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
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
Additional context
a different JWKS verification path.
hardcoding /.well-known/jwks. Alternatively, try /.well-known/jwks.json as a fallback.