Skip to main content

Real-Time & Engine Callbacks

Everything so far is request/response. But an operator needs to see things happen without refreshing — a colleague grabbing the camera, a fresh AI detection, the lease countdown ticking — and the engine needs to tell the API when a move it queued has actually happened. Those are the API's two asynchronous surfaces: pushes out over SignalR, and callbacks in from the engine.

The one idea to hold on to​

The API is both the SignalR hub the frontend subscribes to and the callback target the engine posts to — and together they close the movement loop. Recall from the leasing lesson that a move is queued to the engine and the lease is recorded by a callback. That callback is one of these endpoints, and it also triggers a broadcast:

Broadcasts out: CameraHub​

BroadcastService wraps the SignalR CameraHub. Clients join a group per camera id, and the service pushes to just that group — so an update about one camera reaches only the operators watching it:

await _cameraHub.Clients.Group(cameraId).SendAsync("ReceiveActivityLogBroadcast", message);

That per-camera grouping is the whole real-time model: join the group for the camera you're looking at, receive its activity-log updates, and (from the engine's side) its config broadcasts — the ReceiveCurrentConfigBroadcast that carries the lease countdown the frontend renders.

Callbacks in: FunctionEndpoints​

The engine can't use a user's bearer token — it's a background process. So the endpoints it calls back into are guarded by an API key, not user auth:

if (!_apiKeyValidator.ValidateFrontendApiKey(request)) return Unauthorized();
await Mediator.Send(new LeaseCameraAfterMoveCommand(wrapper));

leaseCameraAfterMove records the lease once a move completes; createMoveHomeRecord does the same for a move-home. Both then fetch the latest activity message and broadcast it to the camera's group — so recording the lease and telling every watching operator about it happen in one hop.

Gotcha: two auth models on one API

User-facing controllers authenticate with the Entra/CIAM bearer token; the FunctionEndpoints authenticate with a shared API key (ValidateFrontendApiKey). They're different trust models for different callers — a human vs the engine. Don't try to reach the function endpoints with a user token, or the engine callbacks with a bearer: each path only accepts its own kind of credential.

Wrapping up​

The API's async side is a matched pair: SignalR's CameraHub broadcasts per-camera updates out to the operators in that camera's group, and API-key-gated FunctionEndpoints let the engine call back to record a lease and trigger the broadcast that tells everyone. Together they close the loop the movement lesson opened — and they run on a different auth model (API key) than the user-facing controllers (bearer).

That leaves the read-heavy majority of the API. Last: The Query Surface.