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