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