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):
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.