Skip to content

Specs your app publishes

Every OpenNVR app self-describes in the two standards its consumers already have tooling for. Both documents are generated from the app's own AppManifest, so they cannot drift from the app: a declared Action is a path, a declared Param is a schema.

opennvr-app spec                     # OpenAPI 3.1, JSON, stdout
opennvr-app spec --format asyncapi   # the NATS surface
opennvr-app spec --yaml -o api.yaml  # for a docs site or a client generator
curl http://my-app:9210/openapi.json # …or ask a running app

OpenAPI 3.1 — the HTTP surface

Served at GET /openapi.json on the app's contract port.

Path Appears when Purpose
GET /health always Liveness plus pipeline vitals; drives the catalog's status dot.
GET /manifest always The declarative identity core stores at registration.
GET /state always Live standing state, documented by the manifest's state_schema.
GET /ui has_ui + ui_mode="internal" The embedded dashboard, rendered sandboxed.
POST /actions/{name} one per declared Action Operator verbs. User-JWT only, via core's proxy.
POST /entitlement/verify entitlement="license_key" Your licence check.

Param typing: Python types map to their JSON equivalents; the catalog's UI types map to the shape they carry on the wire plus an x-opennvr-ui hint, so a generic consumer still sees something usable. geometry.polygon becomes an array of normalized [x, y] vertices; per_camera=True becomes an object keyed by camera id.

AsyncAPI 3.0 — the bus surface

Served at GET /asyncapi.json. Three groups of channels, all derived from the manifest:

  • what the app receivessubscribes, typed by its subject tree, because a Detector on opennvr.inference.>, a DomainEventSubscriber on opennvr.events.plate.recognized.v1.> and an AlertSubscriber on opennvr.alerts.> carry three different envelopes;
  • what it sendsopennvr.alerts.app.<id>.{camera_id}, with the declared alert types and severities;
  • what it requests — one channel per requires_scopes entry, since a scope is how an app asks for PII-bearing domain events, and that belongs in the spec rather than in prose.

The rest of the platform

Core, KAI-C and every AI adapter are FastAPI and publish OpenAPI 3.1 at /openapi.json, with Swagger UI at /docs. The full map, including versioning rules and the deliberate non-goals, is API_STANDARDS.md.

opennvr_app_sdk.openapi

Machine-readable specs for an app's own surfaces.

OpenNVR's server-side APIs are FastAPI, so they already publish OpenAPI 3.1 at /openapi.json (core, KAI-C, every AI adapter) with Swagger UI at /docs. The one surface that had no spec was the one app authors actually implement: the app contract server in :mod:~.contract, which is a stdlib http.server and so generates nothing.

This module closes that. It derives both specs from the app's own :class:~.manifest.AppManifest, which means they cannot drift from the app — a declared action IS a path, a declared param IS a schema, and an app that declares no licence gate has no /entitlement/verify path:

  • :func:contract_openapiOpenAPI 3.1 for the HTTP surface the app serves (/health, /manifest, /state, /ui, /actions/{name}, /entitlement/verify).
  • :func:contract_asyncapiAsyncAPI 3.0 for the NATS surface the app consumes and produces (the inference broadcast it subscribes to, the alert subjects it publishes on, the contracted domain events its requires_scopes grant).

Both are served by the contract server itself at GET /openapi.json and GET /asyncapi.json, and both are printable from the CLI::

opennvr-app spec                     # OpenAPI, to stdout
opennvr-app spec --format asyncapi   # AsyncAPI
opennvr-app spec -o openapi.json     # to a file

Which means every OpenNVR app self-describes in the two standards its consumers already have tooling for: an operator can point Swagger UI or an SDK generator at a running app, and a bus consumer can generate a typed client from the AsyncAPI document.