From c003b21b4ed1fdcd18d8d89a29027bec403f305b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20Caba=C3=A7o?= Date: Fri, 31 Oct 2025 16:52:29 +0000 Subject: [PATCH 1/6] fix(realtime): setAuth not required on custom jwt token When a custom jwt token is provided we no longer require setAuth to be called with the custom jwt --- .../core/supabase-js/src/SupabaseClient.ts | 5 ++ .../core/supabase-js/test/integration.test.ts | 46 ++++++++++++++++ .../test/unit/SupabaseClient.test.ts | 55 +++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 3c2125ce5..7d03910e7 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -156,6 +156,11 @@ export default class SupabaseClient< accessToken: this._getAccessToken.bind(this), ...settings.realtime, }) + if (this.accessToken) { + this.realtime.setAuth().catch((e) => { + console.warn('Failed to set initial Realtime auth token:', e) + }) + } this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, { headers: this.headers, schema: settings.db.schema, diff --git a/packages/core/supabase-js/test/integration.test.ts b/packages/core/supabase-js/test/integration.test.ts index d9553405b..e4f0a4d0b 100644 --- a/packages/core/supabase-js/test/integration.test.ts +++ b/packages/core/supabase-js/test/integration.test.ts @@ -316,6 +316,52 @@ describe('Supabase Integration Tests', () => { expect(receivedMessage).toBeDefined() expect(supabase.realtime.getChannels().length).toBe(1) }, 10000) + + test('should automatically set auth token when using custom JWT without manual setAuth()', async () => { + // Sign up a user with the normal client to get a real JWT token + await supabase.auth.signOut() + const email = `custom-jwt-${Date.now()}@example.com` + const password = 'password123' + const { data: signUpData } = await supabase.auth.signUp({ email, password }) + expect(signUpData.session).toBeDefined() + const realJwtToken = signUpData.session!.access_token + + const customJwtClient = createClient(SUPABASE_URL, ANON_KEY, { + accessToken: async () => realJwtToken, + realtime: { + heartbeatIntervalMs: 500, + ...(wsTransport && { transport: wsTransport }), + }, + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect((customJwtClient.realtime as any).accessTokenValue).toBe(realJwtToken) + + const customChannelName = `custom-jwt-channel-${crypto.randomUUID()}` + const config = { broadcast: { self: true }, private: true } + const customChannel = customJwtClient.channel(customChannelName, { config }) + + expect((customChannel as any).joinPush.payload.access_token).toBe(realJwtToken) + + let subscribed = false + let attempts = 0 + + customChannel.subscribe((status) => { + if (status == 'SUBSCRIBED') subscribed = true + }) + + while (!subscribed) { + if (attempts > 50) throw new Error('Timeout waiting for subscription') + await new Promise((resolve) => setTimeout(resolve, 100)) + attempts++ + } + + expect(subscribed).toBe(true) + expect(customJwtClient.realtime.getChannels().length).toBe(1) + + await customJwtClient.removeAllChannels() + }, 10000) }) }) diff --git a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts index 6f7304114..edaf33c88 100644 --- a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts +++ b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts @@ -259,6 +259,61 @@ describe('SupabaseClient', () => { }) describe('Realtime Authentication', () => { + test('should automatically call setAuth() when accessToken option is provided', async () => { + const customToken = 'custom-jwt-token' + const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect((client.realtime as any).accessTokenValue).toBe(customToken) + expect(customAccessTokenFn).toHaveBeenCalled() + }) + + test('should automatically populate token in channels when using custom JWT', async () => { + const customToken = 'custom-channel-token' + const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + const channel = client.channel('test-channel') + channel.subscribe() + + expect((channel as any).joinPush.payload.access_token).toBe(customToken) + expect((client.realtime as any).accessTokenValue).toBe(customToken) + }) + + test('should handle errors gracefully when accessToken callback fails', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const error = new Error('Token fetch failed') + const failingAccessTokenFn = jest.fn().mockRejectedValue(error) + + const client = createClient(URL, KEY, { accessToken: failingAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to set initial Realtime auth token:', + error + ) + expect(client).toBeDefined() + expect(client.realtime).toBeDefined() + + consoleWarnSpy.mockRestore() + }) + + test('should not call setAuth() automatically in normal mode', async () => { + const client = createClient(URL, KEY) + const setAuthSpy = jest.spyOn(client.realtime, 'setAuth') + + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(setAuthSpy).not.toHaveBeenCalled() + + setAuthSpy.mockRestore() + }) + test('should provide access token to realtime client', async () => { const expectedToken = 'test-jwt-token' const client = createClient(URL, KEY) From 9ac2f5598181ebd3ad8fe0c3ad461743c101a932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20Caba=C3=A7o?= Date: Mon, 3 Nov 2025 15:13:15 +0000 Subject: [PATCH 2/6] test(realtime): fixed ci integration testing for node --- .../core/supabase-js/src/SupabaseClient.ts | 9 ++- .../core/supabase-js/test/integration.test.ts | 81 ++++++++----------- supabase/.branches/_current_branch | 1 + supabase/.temp/cli-latest | 1 + 4 files changed, 40 insertions(+), 52 deletions(-) create mode 100644 supabase/.branches/_current_branch create mode 100644 supabase/.temp/cli-latest diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 7d03910e7..71c8b30d3 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -157,10 +157,13 @@ export default class SupabaseClient< ...settings.realtime, }) if (this.accessToken) { - this.realtime.setAuth().catch((e) => { - console.warn('Failed to set initial Realtime auth token:', e) - }) + setTimeout(() => { + this.accessToken?.() + ?.then((token) => this.realtime.setAuth(token)) + .catch((e) => console.warn('Failed to set initial Realtime auth token:', e)) + }, 0) } + this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, { headers: this.headers, schema: settings.db.schema, diff --git a/packages/core/supabase-js/test/integration.test.ts b/packages/core/supabase-js/test/integration.test.ts index e4f0a4d0b..387849d74 100644 --- a/packages/core/supabase-js/test/integration.test.ts +++ b/packages/core/supabase-js/test/integration.test.ts @@ -1,12 +1,13 @@ +import { assert } from 'console' import { createClient, RealtimeChannel, SupabaseClient } from '../src/index' - +import { sign } from 'jsonwebtoken' // These tests assume that a local Supabase server is already running // Start a local Supabase instance with 'supabase start' before running these tests // Default local dev credentials from Supabase CLI const SUPABASE_URL = 'http://127.0.0.1:54321' const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' - +const JWT_SECRET = 'super-secret-jwt-token-with-at-least-32-characters-long' // For Node.js < 22, we need to provide a WebSocket implementation // Node.js 22+ has native WebSocket support let wsTransport: any = undefined @@ -292,7 +293,7 @@ describe('Supabase Integration Tests', () => { channel .on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload)) - .subscribe((status) => { + .subscribe((status, err) => { if (status == 'SUBSCRIBED') subscribed = true }) @@ -316,52 +317,6 @@ describe('Supabase Integration Tests', () => { expect(receivedMessage).toBeDefined() expect(supabase.realtime.getChannels().length).toBe(1) }, 10000) - - test('should automatically set auth token when using custom JWT without manual setAuth()', async () => { - // Sign up a user with the normal client to get a real JWT token - await supabase.auth.signOut() - const email = `custom-jwt-${Date.now()}@example.com` - const password = 'password123' - const { data: signUpData } = await supabase.auth.signUp({ email, password }) - expect(signUpData.session).toBeDefined() - const realJwtToken = signUpData.session!.access_token - - const customJwtClient = createClient(SUPABASE_URL, ANON_KEY, { - accessToken: async () => realJwtToken, - realtime: { - heartbeatIntervalMs: 500, - ...(wsTransport && { transport: wsTransport }), - }, - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect((customJwtClient.realtime as any).accessTokenValue).toBe(realJwtToken) - - const customChannelName = `custom-jwt-channel-${crypto.randomUUID()}` - const config = { broadcast: { self: true }, private: true } - const customChannel = customJwtClient.channel(customChannelName, { config }) - - expect((customChannel as any).joinPush.payload.access_token).toBe(realJwtToken) - - let subscribed = false - let attempts = 0 - - customChannel.subscribe((status) => { - if (status == 'SUBSCRIBED') subscribed = true - }) - - while (!subscribed) { - if (attempts > 50) throw new Error('Timeout waiting for subscription') - await new Promise((resolve) => setTimeout(resolve, 100)) - attempts++ - } - - expect(subscribed).toBe(true) - expect(customJwtClient.realtime.getChannels().length).toBe(1) - - await customJwtClient.removeAllChannels() - }, 10000) }) }) @@ -404,3 +359,31 @@ describe('Storage API', () => { expect(deleteError).toBeNull() }) }) + +describe('Custom JWT', () => { + describe('Realtime', () => { + test('will connect with a properly signed jwt token', async () => { + const jwtToken = sign({ sub: '1234567890' }, JWT_SECRET, { expiresIn: '1h' }) + const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, { + accessToken: () => Promise.resolve(jwtToken), + }) + await new Promise((resolve) => setTimeout(resolve, 100)) + expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) + let subscribed = false + let attempts = 0 + supabaseWithCustomJwt.channel('test-channel').subscribe((status) => { + if (status == 'SUBSCRIBED') subscribed = true + }) + + // Wait for subscription + while (!subscribed) { + if (attempts > 50) throw new Error('Timeout waiting for subscription') + await new Promise((resolve) => setTimeout(resolve, 100)) + attempts++ + } + + expect(subscribed).toBe(true) + // + }, 10000) + }) +}) diff --git a/supabase/.branches/_current_branch b/supabase/.branches/_current_branch new file mode 100644 index 000000000..88d050b19 --- /dev/null +++ b/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest new file mode 100644 index 000000000..11335d2f8 --- /dev/null +++ b/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.54.11 \ No newline at end of file From 25465d0433af757e4292a4d580146a9668878b97 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 7 Nov 2025 16:53:46 +0200 Subject: [PATCH 3/6] fix(realtime): eliminate race condition in custom JWT token authentication --- .../core/realtime-js/src/RealtimeClient.ts | 14 +++++++- .../core/supabase-js/src/SupabaseClient.ts | 9 +++-- .../core/supabase-js/test/integration.test.ts | 36 ++++++++++--------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/core/realtime-js/src/RealtimeClient.ts b/packages/core/realtime-js/src/RealtimeClient.ts index ece630e12..933a075cc 100755 --- a/packages/core/realtime-js/src/RealtimeClient.ts +++ b/packages/core/realtime-js/src/RealtimeClient.ts @@ -605,7 +605,19 @@ export default class RealtimeClient { private _onConnOpen() { this._setConnectionState('connected') this.log('transport', `connected to ${this.endpointURL()}`) - this.flushSendBuffer() + + // Wait for any pending auth operations before flushing send buffer + // This ensures channel join messages include the correct access token + this._waitForAuthIfNeeded() + .then(() => { + this.flushSendBuffer() + }) + .catch((e) => { + this.log('error', 'error waiting for auth on connect', e) + // Proceed anyway to avoid hanging connections + this.flushSendBuffer() + }) + this._clearTimer('reconnect') if (!this.worker) { diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 71c8b30d3..21bce0a62 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -157,11 +157,10 @@ export default class SupabaseClient< ...settings.realtime, }) if (this.accessToken) { - setTimeout(() => { - this.accessToken?.() - ?.then((token) => this.realtime.setAuth(token)) - .catch((e) => console.warn('Failed to set initial Realtime auth token:', e)) - }, 0) + // Start auth immediately to avoid race condition with channel subscriptions + this.accessToken() + .then((token) => this.realtime.setAuth(token)) + .catch((e) => console.warn('Failed to set initial Realtime auth token:', e)) } this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, { diff --git a/packages/core/supabase-js/test/integration.test.ts b/packages/core/supabase-js/test/integration.test.ts index 387849d74..f9d69df6c 100644 --- a/packages/core/supabase-js/test/integration.test.ts +++ b/packages/core/supabase-js/test/integration.test.ts @@ -367,23 +367,27 @@ describe('Custom JWT', () => { const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, { accessToken: () => Promise.resolve(jwtToken), }) - await new Promise((resolve) => setTimeout(resolve, 100)) - expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) - let subscribed = false - let attempts = 0 - supabaseWithCustomJwt.channel('test-channel').subscribe((status) => { - if (status == 'SUBSCRIBED') subscribed = true - }) - // Wait for subscription - while (!subscribed) { - if (attempts > 50) throw new Error('Timeout waiting for subscription') - await new Promise((resolve) => setTimeout(resolve, 100)) - attempts++ - } + // Wait for subscription using Promise to avoid polling + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Timeout waiting for subscription')) + }, 10000) + + supabaseWithCustomJwt.channel('test-channel').subscribe((status, err) => { + if (status === 'SUBSCRIBED') { + clearTimeout(timeout) + // Verify token was set + expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) + resolve() + } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') { + clearTimeout(timeout) + reject(err || new Error(`Subscription failed with status: ${status}`)) + } + }) + }) - expect(subscribed).toBe(true) - // - }, 10000) + await supabaseWithCustomJwt.removeAllChannels() + }, 15000) }) }) From 4d6b6f6d6e4d2157e03188d681d7c97d9578e198 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 7 Nov 2025 17:19:44 +0200 Subject: [PATCH 4/6] fix(supabase): fix hanging tests in SupabaseClient test suite --- .../test/unit/SupabaseClient.test.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts index edaf33c88..fe35dcf40 100644 --- a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts +++ b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts @@ -259,15 +259,26 @@ describe('SupabaseClient', () => { }) describe('Realtime Authentication', () => { + afterEach(() => { + jest.clearAllMocks() + }) + test('should automatically call setAuth() when accessToken option is provided', async () => { const customToken = 'custom-jwt-token' const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) + const setAuthSpy = jest.spyOn(client.realtime, 'setAuth') - await new Promise((resolve) => setTimeout(resolve, 0)) + // Wait for the constructor's async operation to complete + await Promise.resolve() - expect((client.realtime as any).accessTokenValue).toBe(customToken) + expect(setAuthSpy).toHaveBeenCalledWith(customToken) expect(customAccessTokenFn).toHaveBeenCalled() + + // Clean up + setAuthSpy.mockRestore() + client.realtime.disconnect() }) test('should automatically populate token in channels when using custom JWT', async () => { @@ -275,13 +286,13 @@ describe('SupabaseClient', () => { const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) - await new Promise((resolve) => setTimeout(resolve, 0)) - - const channel = client.channel('test-channel') - channel.subscribe() + // The token should be available through the accessToken function + const realtimeToken = await client.realtime.accessToken!() + expect(realtimeToken).toBe(customToken) + expect(customAccessTokenFn).toHaveBeenCalled() - expect((channel as any).joinPush.payload.access_token).toBe(customToken) - expect((client.realtime as any).accessTokenValue).toBe(customToken) + // Clean up + client.realtime.disconnect() }) test('should handle errors gracefully when accessToken callback fails', async () => { @@ -291,7 +302,9 @@ describe('SupabaseClient', () => { const client = createClient(URL, KEY, { accessToken: failingAccessTokenFn }) - await new Promise((resolve) => setTimeout(resolve, 0)) + // Wait for the promise to reject and warning to be logged + await Promise.resolve() + await Promise.resolve() expect(consoleWarnSpy).toHaveBeenCalledWith( 'Failed to set initial Realtime auth token:', @@ -301,17 +314,18 @@ describe('SupabaseClient', () => { expect(client.realtime).toBeDefined() consoleWarnSpy.mockRestore() + client.realtime.disconnect() }) - test('should not call setAuth() automatically in normal mode', async () => { + test('should not call setAuth() automatically in normal mode', () => { const client = createClient(URL, KEY) const setAuthSpy = jest.spyOn(client.realtime, 'setAuth') - await new Promise((resolve) => setTimeout(resolve, 10)) - + // In normal mode (no accessToken option), setAuth should not be called immediately expect(setAuthSpy).not.toHaveBeenCalled() setAuthSpy.mockRestore() + client.realtime.disconnect() }) test('should provide access token to realtime client', async () => { From 04ce093a2ea5a715cdf79edbc2837abecf0d6812 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 7 Nov 2025 18:51:10 +0200 Subject: [PATCH 5/6] fix(realtime): ensure custom JWT token is set before channel subscriptions --- .../core/realtime-js/src/RealtimeClient.ts | 19 ++++--- .../core/supabase-js/test/integration.test.ts | 56 ++++++++++++------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/core/realtime-js/src/RealtimeClient.ts b/packages/core/realtime-js/src/RealtimeClient.ts index 933a075cc..adebf7f6f 100755 --- a/packages/core/realtime-js/src/RealtimeClient.ts +++ b/packages/core/realtime-js/src/RealtimeClient.ts @@ -186,7 +186,6 @@ export default class RealtimeClient { } this._setConnectionState('connecting') - this._setAuthSafely('connect') // Establish WebSocket connection if (this.transport) { @@ -253,11 +252,13 @@ export default class RealtimeClient { this._setConnectionState('disconnected') } - // Close the WebSocket connection - if (code) { - this.conn.close(code, reason ?? '') - } else { - this.conn.close() + // Close the WebSocket connection if close method exists + if (typeof this.conn.close === 'function') { + if (code) { + this.conn.close(code, reason ?? '') + } else { + this.conn.close() + } } this._teardownConnection() @@ -608,7 +609,11 @@ export default class RealtimeClient { // Wait for any pending auth operations before flushing send buffer // This ensures channel join messages include the correct access token - this._waitForAuthIfNeeded() + const authPromise = + this._authPromise || + (this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve()) + + authPromise .then(() => { this.flushSendBuffer() }) diff --git a/packages/core/supabase-js/test/integration.test.ts b/packages/core/supabase-js/test/integration.test.ts index f9d69df6c..3d21d6271 100644 --- a/packages/core/supabase-js/test/integration.test.ts +++ b/packages/core/supabase-js/test/integration.test.ts @@ -363,31 +363,45 @@ describe('Storage API', () => { describe('Custom JWT', () => { describe('Realtime', () => { test('will connect with a properly signed jwt token', async () => { - const jwtToken = sign({ sub: '1234567890' }, JWT_SECRET, { expiresIn: '1h' }) + const jwtToken = sign( + { + sub: '1234567890', + role: 'anon', + iss: 'supabase-demo', + }, + JWT_SECRET, + { expiresIn: '1h' } + ) const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, { accessToken: () => Promise.resolve(jwtToken), + realtime: { + ...(wsTransport && { transport: wsTransport }), + }, }) - // Wait for subscription using Promise to avoid polling - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for subscription')) - }, 10000) - - supabaseWithCustomJwt.channel('test-channel').subscribe((status, err) => { - if (status === 'SUBSCRIBED') { - clearTimeout(timeout) - // Verify token was set - expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) - resolve() - } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') { - clearTimeout(timeout) - reject(err || new Error(`Subscription failed with status: ${status}`)) - } + try { + // Wait for subscription using Promise to avoid polling + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Timeout waiting for subscription')) + }, 4000) + + supabaseWithCustomJwt.channel('test-channel').subscribe((status, err) => { + if (status === 'SUBSCRIBED') { + clearTimeout(timeout) + // Verify token was set + expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) + resolve() + } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') { + clearTimeout(timeout) + reject(err || new Error(`Subscription failed with status: ${status}`)) + } + }) }) - }) - - await supabaseWithCustomJwt.removeAllChannels() - }, 15000) + } finally { + // Always cleanup channels and connection, even if test fails + await supabaseWithCustomJwt.removeAllChannels() + } + }, 5000) }) }) From 793bbdee3c9005da9a1a922ead27f100f3792722 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 7 Nov 2025 19:04:10 +0200 Subject: [PATCH 6/6] fix(realtime): eliminate race condition in custom JWT token authentication --- packages/core/realtime-js/src/RealtimeClient.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/realtime-js/src/RealtimeClient.ts b/packages/core/realtime-js/src/RealtimeClient.ts index adebf7f6f..3254fafed 100755 --- a/packages/core/realtime-js/src/RealtimeClient.ts +++ b/packages/core/realtime-js/src/RealtimeClient.ts @@ -187,6 +187,13 @@ export default class RealtimeClient { this._setConnectionState('connecting') + // Trigger auth if needed and not already in progress + // This ensures auth is called for standalone RealtimeClient usage + // while avoiding race conditions with SupabaseClient's immediate setAuth call + if (this.accessToken && !this._authPromise) { + this._setAuthSafely('connect') + } + // Establish WebSocket connection if (this.transport) { // Use custom transport if provided