-
Notifications
You must be signed in to change notification settings - Fork 486
Description
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_OUT
after 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 updates
Debug Information
Network monitoring shows the WebSocket connection works perfectly:
- ✅ WebSocket connects successfully
- ✅ Sends
phx_join
message - ✅ Server responds with
phx_reply
statusok
- ❌ 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).