Skip to content

Events and realtime

Every write in fxquinox emits an event, so clients can react to changes instead of polling. This is what keeps the web UI live, and you can tap the same stream. Each mutation in services/records.py writes an EventLog row and broadcasts the event on an in-process bus — so the persistent log and the live stream stay in lockstep.

What an event looks like

Each event carries the entity type, the entity id, the action (create / update / delete), the actor (the user who triggered it), and the changes that were applied:

{
  "entity_type": "Task",
  "entity_id": "a1b2c3…",
  "action": "update",
  "actor": "Demo User",
  "changes": {"status": "inProgress"}
}

The WebSocket stream

Connect to /events/ws and you receive one JSON message per change as it happens. Browsers cannot set an Authorization header on a WebSocket, so the bearer token (a login token or an fxq_ API key) is passed as a ?token= query parameter and validated before the socket is accepted — an invalid or missing token closes the connection with code 1008.

import asyncio, json, websockets

TOKEN = "fxq_..."  # see Authentication

async def watch():
    url = f"ws://localhost:8000/events/ws?token={TOKEN}"
    async with websockets.connect(url) as ws:
        async for message in ws:
            event = json.loads(message)
            print(event["action"], event["entity_type"], event["entity_id"])

asyncio.run(watch())

See Authentication and API keys for how to obtain a token.

Recent events (polling)

If you only need a catch-up rather than a live feed, GET /events?limit=50 returns the most recent rows from the event_log table (newest first):

curl "http://localhost:8000/events?limit=50" -H "Authorization: Bearer <token>"

In-process subscribers (backend)

Inside the backend, the event bus is a small publish/subscribe API (fxq.events.subscribe / emit / unsubscribe, in events/bus.py). Callbacks run synchronously when emit is called, and an exception in one subscriber can never disrupt the others. The WebSocket endpoint is itself just a subscriber that forwards events to connected clients.

For side effects that shouldn't sit on the request path — outbound webhooks and chat deliveries — a background dispatcher (services/dispatch.py) drains a queue on its own thread with its own DB session, so delivery latency or failures never block or roll back the originating write. It is wired up by the production entrypoints (dev_server.py / seed_serve.py).

Build reactive automation

Common uses: notify a chat channel when a shot is approved, kick off a transcode when a version is published, or sync status to another system.