Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ricardomb-tech/surqo/llms.txt

Use this file to discover all available pages before exploring further.

Surqo’s WebSocket server broadcasts new sensor readings to every connected client the moment they arrive from an ESP32 node over MQTT. There is no polling, no delay, and no missed readings — the WebSocketManager maintains a dictionary of active connections keyed by farm_id and fans out each ingest event to all subscribers for that farm simultaneously. The connection is farm-scoped: each tab or client subscribes to exactly one farm at a time.
The Surqo dashboard and the main landing page (index.mdx) use this endpoint to power the live sensor feed widgets. Every time a new reading arrives from the field, the dashboard KPI cards, soil moisture chart, and VPD indicator update in real time without a page refresh.

Connection Details

PropertyValue
Endpointwss://surqo-api.fly.dev/api/v1/sensors/ws/live/{farm_id}
AuthJWT token as ?token=<jwt> query parameter
ProtocolStandard WebSocket (no subprotocol required)
Message formatUTF-8 JSON text frames
Authentication uses the same Supabase-issued JWT as the REST API, but passed as a query parameter rather than a header (the WebSocket handshake does not support custom headers in browsers). The server validates the token, looks up the farm, and confirms ownership before accepting the connection.

JavaScript Example

const farmId = 'your-farm-uuid'
const token = await supabase.auth.getSession()
  .then(s => s.data.session?.access_token)

const ws = new WebSocket(
  `wss://surqo-api.fly.dev/api/v1/sensors/ws/live/${farmId}?token=${token}`
)

ws.onopen = () => {
  console.log('✅ Connected to Surqo live feed')
}

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)

  if (msg.type === 'initial') {
    // Last known reading sent immediately on connection
    console.log('Last reading:', msg.data)
  }

  if (msg.type === 'sensor_reading') {
    // New reading from ESP32 (via MQTT)
    const { soil_moisture_pct, air_temp_c, vpd_kpa } = msg.data
    console.log(`Soil: ${soil_moisture_pct}% | Temp: ${air_temp_c}°C | VPD: ${vpd_kpa} kPa`)
  }
}

ws.onclose = (event) => {
  console.log('Disconnected, code:', event.code)
  // Reconnect logic here if needed
}

Message Types

typeWhen sentData
initialImmediately on connectionLast persisted sensor reading (SensorReadingResponse)
sensor_readingWhen a new MQTT reading arrivesPartial reading object with core sensor metrics
On connection the server sends an initial message containing the most recent reading stored in the database. This allows the UI to render current values immediately without a separate REST call. Every subsequent sensor_reading message carries the live payload broadcast by the MQTT consumer as readings arrive from the field.

Close Codes

CodeReason
4001JWT token missing or invalid
4002Invalid farm UUID format
4003Farm not found or not owned by the authenticated user
Standard WebSocket close codes (1000 normal, 1001 going away, etc.) are also used for non-auth disconnects.

TypeScript Frontend Utility

The frontend wraps the WebSocket in a helper that handles message routing:
// Simplified from frontend/src/lib/websocket.ts
function createLiveFeed(
  farmId: string,
  token: string,
  onReading: (data: any) => void
): WebSocket {
  const wsUrl = process.env.NEXT_PUBLIC_WS_URL
  const ws = new WebSocket(
    `${wsUrl}/api/v1/sensors/ws/live/${farmId}?token=${token}`
  )

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    if (msg.type === 'sensor_reading' || msg.type === 'initial') {
      onReading(msg.data)
    }
  }

  return ws
}
Set NEXT_PUBLIC_WS_URL=wss://surqo-api.fly.dev in production and ws://localhost:8000 in local development.

Reconnection Guidance

If no new MQTT readings arrive — for example, the ESP32 is in deep sleep for its default 15-minute cycle — the WebSocket stays open but idle. The server does not send keepalive pings. For production use, implement a heartbeat check on the client side and reconnect automatically if no message is received within a reasonable window (e.g. 20 minutes).
A minimal reconnect pattern:
let ws
let heartbeatTimer

function connect(farmId, token) {
  ws = new WebSocket(
    `wss://surqo-api.fly.dev/api/v1/sensors/ws/live/${farmId}?token=${token}`
  )

  ws.onopen = () => {
    clearInterval(heartbeatTimer)
    heartbeatTimer = setInterval(() => {
      if (ws.readyState !== WebSocket.OPEN) {
        connect(farmId, token) // reconnect
      }
    }, 20 * 60 * 1000) // check every 20 min
  }

  ws.onclose = (event) => {
    if (event.code !== 4001 && event.code !== 4003) {
      // Auth errors should not be retried — redirect to login instead
      setTimeout(() => connect(farmId, token), 5000)
    }
  }

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    // handle msg.type === 'initial' and msg.type === 'sensor_reading'
  }
}
Close codes 4001 and 4003 indicate authentication or authorization failures. Do not retry on these codes — the JWT may have expired. Fetch a fresh token from Supabase and reconnect, or redirect the user to the login page.

Build docs developers (and LLMs) love