Skip to main content

Writing Back

Every page so far has the frontend reading Esri: tokens, WebMaps, feature queries, stream sockets. But the Velocity beams on the previous page did not appear by magic, the Alerts layer holds features an operator typed into our own form, and the public CameraLocations service knows where every camera is pointing. Something wrote those. This page is the backend half of the contract, written for the frontend engineer who needs to know, when the map disagrees with the database, _which writer last touched the layer in question. The services live in the ImageAcquisition mono repo and the shared Infrastructure package, so their own chapters hold the line-by-line detail; what follows is the map of the writers and the properties that shape them.

The one idea to hold on to​

Esri is a projection, not a store, and five writers feed it over two transports. Two Event Hubs carry the high-frequency telemetry that ArcGIS Velocity turns into stream layers: camera position and status on esri_cameras, localized AI hits on esri_velocity. One REST client, ESRIClient, writes the three hosted feature services the map reads directly: Alerts, Camera_Locations and Pulse_Point. Nothing on the frontend writes to Esri at all. Press the events and watch each packet take the only route it can:

The write boardPress an event, then read the payload that actually crosses the wire. Toggle UseEventHub and age the detection to see the gates.
cameras → esri_camerasAI → Velocityalerts → Alerts servicecameras → Camera_Locationsincidents → Pulse_Point
0 in flight

Press an event. The packet takes the only route it can, and the payload that actually crosses the wire appears here.

Cameras: the esri_cameras hub​

The capture orchestration in ImageAcquisition is the only writer of camera telemetry, and it writes two kinds of event. A position event leaves on every capture pass that finds the durable entity's IsPositionChanged flag set, fire-and-forget so a slow hub can never stall a capture, carrying pan, tilt, zoom, the camera's model and a horizontal field of view computed from that model's measured optics curve at the event's zoom. The curve is cached for fifteen minutes because position events fire on every cycle and a lens is a physical constant. Velocity's Arcade reads hfov straight off the message and draws the viewshed wedge the previous page hover-gated, which is why the backend does the optics and the GIS side never re-derives them.

A status event is rarer by design. The orchestration knows a camera is offline when it runs more than sixty seconds past its latest image, but it publishes that only on the edge: the sync entity remembers the last status sent, so an offline camera produces one isOnline: 0 message, not one per pass, and one isOnline: 1 when it returns. Status events carry no position and no hfov; the cone stays where the last position event put it.

Both kinds carry moveExpirationDate, read from the operational document that Frontend.API writes when an operator takes manual control. That is the same MovementExpirationDate behind the client-side isRecentlyMoved arrow, so the stream layers and our own arrows agree on which cameras are "recently moved" because they read one Cosmos field. The one subtlety: Frontend.API writes that field after the move completes, so the position event can beat the write. While the orchestration is in its leased mode the activity assumes a move just happened and sends the default two-hour window rather than a null that would stick until the next change.

AI hits: the esri_velocity hub, and when it goes quiet​

The Notification app ingests Condor detections, and in its polling mode it is the writer of the AI hit lines on the map. After a detection's COCO boxes are enriched with the localization call, each box that produced a fire distance, and only while the detection is under ten minutes old, becomes one message on esri_velocity: the camera, the estimated fire point, a two-vertex line from one to the other, the confidence and localization scores, and whether it came from a 60° frame or a 360° pano. Older or unlocalized detections are stored in SQL and shown on tiles, but Velocity never hears about them.

Flip CondorEventSourceOptions:UseEventHub on the board and the Notification lane stands down. In that mode Condor publishes to condor_hub itself and Velocity reads it through its own consumer group, so if Notification also forwarded every hit the map would draw each one twice. The publish is skipped with a continue, and the only heartbeat that tells you which mode an environment is really in is the AiIngestion.ActivePath metric, emitted from whichever lane is processing. A misconfigured flag produces silence, not an error, which is the thing to remember when AI lines stop appearing after a deployment.

Alerts: addFeatures, then best effort​

When an operator sends an alert, Frontend.API's create command writes the SQL row and its origin circle first, uploads the captured map image to blob storage, looks up the what3words address, and only then posts one feature to the Alerts service with addFeatures. The attributes are the ones the frontend's Alerts layer renders: the SQL id, the creator and organisation, the message, radius, location context, the last five frames from the camera as a comma-joined list, and the what3words square, omitted when the lookup failed so the field is simply empty.

Moving an alert is where the asymmetry shows. The update command treats SQL as authoritative and the Esri write as best effort: it queries the whole service, matches on the stored id, updates the feature's geometry and radius, and if any of that fails it logs and returns success. The map may lag the database for a moment; the database never lags the map. One detail worth stealing: on update the what3words field is set to an empty string, not null, because the client serialiser omits nulls and an omitted field would have left the old address on the moved alert.

Camera_Locations: three writers, one shape​

The Camera_Locations service is the layer the frontend draws its camera points from under the client source: the arrows, wedges and lines are ours, but the point itself, its site name and the pan, tilt and zoom the Admin health card compares against Cosmos come from this service. It has three writers and they all use the same upsert. Frontend.API's camera control service upserts a camera's pan, tilt and zoom after every move, focus or brightness command. The Admin API upserts or deletes cameras in bulk when their public visibility is toggled, and exposes a per-camera upsert and delete for a relocation or decommission. And the Admin API's map sync service rebuilds the whole layer on demand.

Upsert versus full syncRun each path and scrub. Then break the query response and compare the old behaviour with today's.

The upsert reads the whole layer to learn which cameras already exist, keeps their OBJECTIDs and replaces attributes and geometry with updates, then adds the rest. Cheap per call, and the same shape whether Frontend.API sends one camera after a move or the Admin API sends a bulk visibility change.

The upsert reads the entire layer with a 1=1 query to learn which cameras exist, partitions the batch by camera id, posts updates for the existing ones with their object ids preserved, and adds for the rest. That read is the hazard the demo shows: a response that failed to deserialise used to come back as null, callers coalesced null to an empty collection, and an empty "existing" set made every camera look new. One malformed response duplicated the layer. The query now throws and counts a failure, which is a good example of the rule that an empty result from Esri must never be trusted without knowing why it is empty.

Full sync is the reconciler, not the fast path. It fetches the fleet from the environment's own public API, converts each camera (skipping any with null or zero coordinates and deriving isOnline from whether the latest image is under a minute old), mints a token, queries every object id, deletes them in batches of five hundred and re-adds the fleet in batches of five hundred. Object ids change, and anything the converter skipped vanishes from the map. It is the tool for a layer that has drifted, and it is audited step by step so the operator can see where it stopped.

Incidents: PulsePoint​

PulsePoint pushes every incident and unit status change to a function in CameraOperation. The function keeps a Cosmos record per incident number, folds unit changes into per-status lists, and upserts one feature into the Pulse_Point service with the status Active, or Inactive when the push carries a close time. Pushes without coordinates are stored and never sent, because Esri cannot place them. A daily job in production marks anything a day stale as inactive, deletes features a week old along with any duplicates by incident number, and an Admin reconcile marks every feature with no Cosmos record inactive. The frontend's PulsePoint layer therefore shows the backend's view of "active", not PulsePoint's.

One client, one hub service​

Everything on the REST side goes through ESRIClient in the Infrastructure package, and three of its habits matter to anyone reading telemetry. It authenticates with a bearer API key and a Referer header, because the hosted services are referrer-locked. It inspects every 200 body for Esri's error object, because ArcGIS answers success for a large class of failures, and counts the result in the Esri.Request metric by operation and outcome, which is the only place a 200-with-error becomes visible. And it retries 429 and 503 up to five times, honouring Retry-After and otherwise backing off two, four, eight seconds with jitter, the server-side twin of the request budget on the frontend.

The hub side is one EventHubService registered three times, once per hub, behind marker interfaces so a function asks for the cameras hub or the velocity hub by type. Messages on the two Esri hubs are stamped application/json with a fresh message and correlation id, which Velocity uses for de-duplication, and a message that exceeds a batch throws rather than being split. The hubs themselves are two partitions each in the image-acquisition namespace, with a consumer group per hub for Velocity, and the functions send through their managed identities under the Event Hubs Data Sender role rather than connection strings.

Wrapping up​

Esri holds projections of our data. Camera telemetry streams through esri_cameras and localized AI hits through esri_velocity, where Velocity turns them into the stream layers and their persisted companions; alerts, camera locations and incidents are written to hosted feature services through one REST client with one error discipline. Position events are per pass, status events are per edge, AI hits are gated by freshness and localization, alert updates are best effort, and Camera_Locations has three writers sharing an upsert plus a reconciler that rebuilds it. When the map is wrong, name the writer first.

Where the mechanism lives​

The backend chapters carry the full accounts: the Notification ingestion lanes in Getting Detections In, the Admin API's reconciler in Syncing to the Map, and the incident feed in PulsePoint.

Next: Hit Tests and the Click Budget, how the map listens.