Skip to content

WebSocket

Norn uses WebSocket for real-time event broadcasting. The dashboard and CLI connect to receive live updates during deploys, restarts, and other operations.

Connection

text
ws://localhost:8800/api/v1/events
wss://norn.example.com/api/v1/events

/api/v1/events is the versioned endpoint. /ws remains as a compatibility alias. When NORN_API_TOKEN is configured, connections require either the control-plane token or an access token carrying events:read in the Authorization: Bearer header. Cloudflare Access sessions continue to use the validated Access cookie/header path. Norn does not accept bearer tokens in URL query parameters.

Origin checking allows native clients with no Origin header, localhost, and origins in NORN_ALLOWED_ORIGINS. The formal norn.exec/v1 WebSocket uses the same origin policy, requires apps:exec, and is created through a device-key step-up flow. See Native control protocol.

Event Envelope

Every message is a JSON object with this structure:

json
{
  "id": 4812,
  "timestamp": "2026-08-07T21:30:00Z",
  "type": "deploy.step",
  "appId": "myapp",
  "payload": {
    "step": "build",
    "sagaId": "abc-123",
    "status": "running"
  }
}
FieldTypeDescription
idintegerDurable monotonic cursor
timestampRFC3339 stringTime the control event was recorded
typestringEvent type identifier
appIdstringApp this event relates to
payloadobjectType-specific data

Event Types

TypePayload FieldsEmitted When
deploy.stepstep, sagaId, statusPipeline step starts, completes, or fails
deploy.progresssagaId, messageAllocation health polling updates
deploy.completedsagaId, imageTagDeploy pipeline finished successfully
deploy.failedsagaId, errorDeploy pipeline failed
app.restartedsagaIdAllocation replacement accepted
app.scaledsagaId, group, countTask group scaled
function.completedexecutionId, statusFunction invocation finished
maintenance.startedoperationId, kind, statusHost agent claimed a platform or host operation
maintenance.completedoperationId, kind, statusMaintenance operation succeeded
maintenance.failedoperationId, kind, status, messageMaintenance operation failed

Cursor Replay

Events are stored in PostgreSQL before being broadcast. Reconnect with the last processed event ID to replay missed messages:

text
wss://norn.example.com/api/v1/events?after=4812

Replay is capped at 500 events per connection. If a client receives 500 replay events, it should reconnect with the last ID until caught up. REST operation state remains authoritative; the event stream tells clients what changed.

Before reconnecting, read GET /api/v1/events/info for retention bounds and supported stream features. types and apps provide comma-separated, exact-match subscriptions. heartbeat opts into a 10–120 second liveness frame. A stale cursor is rejected before upgrade with event_cursor_gap; an ahead cursor uses event_cursor_ahead. Both include current bounds so the client can reconcile REST state deliberately.

Step Status Values

The deploy.step event's status field:

StatusMeaning
runningStep is currently executing
completeStep finished successfully
failedStep encountered an error

Hub Architecture

The WebSocket hub uses gorilla/websocket and manages connections with a central broadcast loop:

  • Hub.Run() — goroutine running the main select loop (register, unregister, broadcast)
  • Hub.Broadcast(evt) — persists the event, assigns its cursor, and sends it to connected clients
  • External event polling — relays events written by the independent host agent
  • Per-client send buffer — buffered channel (64 messages) prevents slow clients from blocking others
  • Broadcast channel — buffered at 256 messages
  • Cleanup — if a client's send buffer is full, the client is disconnected and cleaned up

Origin Checking

The upgrader checks the Origin header:

  1. Empty origin → allowed (non-browser clients)
  2. Origin in the allowed origins list → allowed
  3. Origin hostname is localhost, 127.0.0.1, or ::1 → allowed
  4. Otherwise → rejected

Default allowed origins: http://localhost:5173, http://localhost:3000. Additional origins are configured via NORN_ALLOWED_ORIGINS (comma-separated).

CLI Integration

The CLI connects to the WebSocket during operations like norn deploy to render live progress. It uses the Bubble Tea channel pattern:

  1. Goroutine connects to WebSocket and reads messages
  2. Messages are sent to a Go channel
  3. A Bubble Tea command wraps the channel read as waitForEvent
  4. The TUI model updates on each received event
go
// Simplified pattern
go func() {
    for {
        _, msg, err := conn.ReadMessage()
        if err != nil { return }
        eventCh <- msg
    }
}()

func waitForEvent(ch chan []byte) tea.Cmd {
    return func() tea.Msg {
        return wsMsg(<-ch)
    }
}