Concepts¶
The four archetypes¶
An app is defined by where its input comes from. Pick by that, not by what the app does.
| Archetype | Input | You implement | Cost |
|---|---|---|---|
Detector |
opennvr.inference.> — results another app is already driving |
on_detections(camera_id, detections, event) |
none: the GPU is already paid |
FrameApp |
frames you poll, inference you call | on_frame(camera_id, jpeg) |
one inference per frame, yours |
DomainEventSubscriber |
contracted domain events (plate.recognized.v1) |
on_event(DomainEvent) |
none |
AlertSubscriber |
opennvr.alerts.> |
on_alert(alert, subject) |
none |
App — the facade — compiles to a Detector, which is the right
starting point for most apps: the platform is usually already running
the model you need, and riding its stream is free.
Reach for FrameApp only when nothing on the deployment produces what
you need and you must run a model yourself.
Alerts and events are different things¶
- An alert is for a human. It lands in the operator inbox, it has a severity, and someone is expected to look at it. Envelope: §11.5.
- A domain event is for another app. It is versioned, contracted, and nobody is expected to read it. Envelope: EVENT_CONTRACTS.md.
An app that recognises plates publishes a domain event; a gate controller, a visitor log and a dashboard all react to it without any of them knowing the plate reader exists. Only the gate controller fires an alert, and only when the plate is unknown.
Declare, don't build¶
The SDK's central bet is that an app which declares its surfaces
needs no frontend. From AppManifest the App Catalog builds:
- the config form from
params— including a zone editor on a camera still forgeometry.polygon; - the dashboard from
state_schema— metrics, gauges, tables, logs, galleries, over whateverstate_snapshot()returns; - the buttons from
actions, proxied toPOST /actions/{name}; - the store listing from
description,use_cases,pricing; - the licence gate from
entitlement.
The same manifest generates your app's OpenAPI and AsyncAPI documents, so the spec cannot drift from the app either.
Identity and scope¶
An app never holds the deployment's site key. At registration core
issues it a credential of its own, scoped to the cameras an operator
assigned it. nvr.cameras() returns that roster, not the site's — an
app cannot see everything by accident, and an operator can see exactly
what each app can reach.
Egress works the same way: apps sit on an internal network with no direct route out, and outbound traffic goes through the deployment's proxy against a host allow-list the operator can audit. That is what makes a third-party app installable in a hospital or a ministry.
The event timeline, not the wall clock¶
Rules that measure duration should use the event's own timestamp
(Detector.parse_event_ts, or event.ts on the facade), never
time.time(). A replayed, delayed or reordered event must not skew a
dwell timer. keyed_state takes at= for exactly this reason.