WebSocket Race Condition in Supabase JS Client - Node.js Only
Opened this issue ยท 0 comments
Description:
The Supabase JS client has a race condition in Node.js environments where realtime subscriptions consistently timeout due to improper event handler registration timing when global.WebSocket is undefined.
Root Cause
When global.WebSocket is undefined (Node.js environments), the Supabase client falls back to the ws module but has a race condition where:
- WebSocket connects and server responds immediately with
phx_reply - Client hasn't finished setting up response handlers yet
- Response gets lost โ
TIMED_OUTafter 10 seconds
Environment
- Node.js Version: 21.1.0 (also tested with 20.x LTS)
- Supabase JS Version: 2.36.0 (also tested with 2.38.0)
- Operating System: macOS (Darwin 23.5.0)
- Platform: Node.js only (browsers work fine due to native
global.WebSocket)
Steps to Reproduce
- Create a simple Supabase client with realtime subscription:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
const channel = supabase
.channel("my_table")
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "my_table"
},
(payload) => {
console.log("Change detected:", payload)
}
)
.subscribe((status, err) => {
console.log("Status:", status)
})- Run the code
- Observe timeout after ~10 seconds
Expected Behavior
The WebSocket should connect successfully and subscribe to postgres changes, similar to raw WebSocket implementation.
Actual Behavior
๐ก Subscription status: TIMED_OUT
โ Connection timed out. Trying to reconnect...
Working Raw WebSocket Implementation
The same realtime endpoint works perfectly with raw WebSocket:
import WebSocket from 'ws'
const wsUrl = SUPABASE_URL.replace('https://', 'wss://') + '/realtime/v1/websocket'
const params = new URLSearchParams({
apikey: SUPABASE_ANON_KEY,
vsn: '1.0.0'
})
const ws = new WebSocket(wsUrl + '?' + params.toString())
ws.on('open', () => {
console.log('โ
WebSocket connected')
ws.send(JSON.stringify({
topic: 'realtime:my_table',
event: 'phx_join',
payload: {
config: {
postgres_changes: [
{ event: '*', schema: 'public', table: 'my_table' }
]
}
},
ref: 1
}))
})
// This works perfectly and receives real-time updatesDebug Information
Network monitoring shows the WebSocket connection works perfectly:
- โ WebSocket connects successfully
- โ
Sends
phx_joinmessage - โ
Server responds with
phx_replystatusok - โ Client doesn't receive/process the response due to race condition
Analysis shows global.WebSocket is undefined in Node.js, causing the client to use a different code path with timing issues.
Proof of Race Condition
Adding a simple WebSocket monkey patch fixes the issue:
// This fixes the race condition
const OriginalWebSocket = global.WebSocket || (await import('ws')).default;
class PatchedWebSocket extends OriginalWebSocket {
constructor(url, protocols, options) {
super(url, protocols, options);
}
}
global.WebSocket = PatchedWebSocket;
// Now Supabase client works correctly
const supabase = createClient(url, key);Additional Context
- RLS is disabled on the target table
- Table is properly enabled for realtime replication
- API credentials work fine for REST operations
- Raw WebSocket implementation works perfectly
- Issue only occurs in Node.js environments
- Browsers work fine due to native
global.WebSocket
Workaround
Set global.WebSocket to a custom class before creating the Supabase client (see code above).