WebSocket Realtime
The terminal listens for new scale commands via a raw WebSocket connection to Supabase Realtime. The connection is receive-only from the terminal’s perspective.
Connection URLs
Production:
wss://<SUPABASE_PROJECT_ID_PROD>.supabase.co/realtime/v1/websocket?apikey=<SUPABASE_PUBLISHABLE_KEY_PROD>&vsn=1.0.0
Development:
wss://<SUPABASE_PROJECT_ID_DEV>.supabase.co/realtime/v1/websocket?apikey=<SUPABASE_PUBLISHABLE_KEY_DEV>&vsn=1.0.0
| Environment | Project ID | Where to find keys |
|---|---|---|
| Production | aqbchetcfyqtyrdjznvz | Supabase Setup → .env.prod |
| Development | jfmdrdleyyclgqjjowxt | Supabase Setup → .env.dev |
The publishable (anon) keys are in supabase/functions/.env.prod / .env.dev.
Step 1 — Send phx_join on connect
Replace <SCALE_UUID> with the terminal’s scale ID.
{
"topic": "realtime:scale-<SCALE_UUID>",
"event": "phx_join",
"payload": {
"config": {
"broadcast": { "ack": false, "self": false },
"presence": { "key": "", "enabled": false },
"postgres_changes": [
{
"event": "INSERT",
"schema": "public",
"table": "scale_commands",
"filter": "scale_id=eq.<SCALE_UUID>"
}
],
"private": false
}
},
"ref": "1",
"join_ref": "1"
}Server confirms with two messages:
{ "event": "phx_reply", "payload": { "status": "ok" } }
{ "event": "system", "payload": { "status": "ok", "message": "Subscribed to PostgreSQL" } }Step 2 — Heartbeat every 25 seconds
Without a heartbeat the server silently closes the connection after ~30 seconds.
{
"topic": "phoenix",
"event": "heartbeat",
"payload": {},
"ref": "2"
}Increment ref with each message sent. Server replies with phx_reply + status: ok.
Step 3 — Handle incoming command events
{
"event": "postgres_changes",
"payload": {
"data": {
"table": "scale_commands",
"type": "INSERT",
"record": {
"id": "<command-uuid>",
"scale_id": "<scale-uuid>",
"name": "push_weight",
"payload": {},
"status": "pending"
}
}
}
}On receipt:
- Read
nameto determine the action (push_weightorupload_photos) - Execute the action
- Complete the command by passing
commandIdin the corresponding API call
Step 4 — Reconnect on disconnect
- Stop heartbeat timer
- Wait with exponential backoff: 3 → 6 → 12 → 24 → max 60 s
- Reconnect to WebSocket
- Re-send
phx_join(server does not retain subscriptions between connections) - Restart heartbeat timer
- Poll
GET /api/terminal/scales/{scaleId}/commandsfor any commands missed during downtime
Supabase JS Client (Alternative)
If using the Supabase JS client instead of raw WebSocket:
// Subscribe to scale updates (sessions, status changes)
const scaleChannel = supabase
.channel('scales')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'scales',
filter: `id=eq.${scaleId}`
}, (payload) => {
handleScaleUpdate(payload.new);
})
.subscribe();
// Subscribe to new commands
const commandsChannel = supabase
.channel('scale_commands')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'scale_commands',
filter: `scaleId=eq.${scaleId}`
}, (payload) => {
handleNewCommand(payload.new);
})
.subscribe();Related
- scale-commands — what to do when a command arrives
- order-flows — how WebSocket events fit into the user-driven flow
- integration-guide — full integration overview