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
EnvironmentProject IDWhere to find keys
ProductionaqbchetcfyqtyrdjznvzSupabase Setup → .env.prod
DevelopmentjfmdrdleyyclgqjjowxtSupabase 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:

  1. Read name to determine the action (push_weight or upload_photos)
  2. Execute the action
  3. Complete the command by passing commandId in the corresponding API call

Step 4 — Reconnect on disconnect

  1. Stop heartbeat timer
  2. Wait with exponential backoff: 3 → 6 → 12 → 24 → max 60 s
  3. Reconnect to WebSocket
  4. Re-send phx_join (server does not retain subscriptions between connections)
  5. Restart heartbeat timer
  6. Poll GET /api/terminal/scales/{scaleId}/commands for 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();