# Surveillance — enterprise architecture (build knowledge)

How the Leumas surveillance system is built and how to extend it. It scales from a single box to millions of cameras across many licensed businesses via a control-plane / data-plane split, and every...


How the Leumas surveillance system is built and how to extend it. It scales from a single box to
millions of cameras across many licensed businesses via a **control-plane / data-plane split**, and every
detection capability is a **plugin**. Read this before touching surveillance.

## The shape

```
CONTROL PLANE  (leumas-api — touches EVENTS, not raw video)
  tenant/fleet/site camera registry · rule engine · licensing/fleet keys · PassNode metering ·
  entitlement quotas · live event index (SSE /api/surveillance/stream + WS /ws/surveillance) · DVR index
        ▲ pull cameras            │ publish events + heartbeat (keyed)         ▲ live walls / rules
        │ GET /node/cameras       ▼ POST /ingest (per-node/-device auth)       │
DATA PLANE  (edge detection nodes — run the CV near the cameras)
  @leumas/surveillance-node  ── reuses the SAME camera-manager services (media/detector/recorder) ──
                             ── loads enabled VISION RECIPES per camera ── keyed publish SDK
  (the API's in-process sidecars ARE "the built-in node" for a single-box/EXE install — no fork)
```

**Never haul raw video to a central GPU.** Detection runs near the cameras; only events (+ optional
clips) reach the control plane. Edge-first is the whole scaling story.

## Where things live
- Engine (connector-free pipelines + registry): `shared/engines/adapters/surveillance/camera-manager/`
  — `services/{store,detections,detector,media,recorder,streams,flask,recipe-registry,recipes.builtin,domain-recipes,geometry,trigger,match,boot}.js`, `python/flask_server.py`, `index.js` (adapter tools).
- Control-plane HTTP: `products/leumas-api/src/routes/surveillance.js`; WS: `products/leumas-api/src/ws/surveillance.js`.
- Edge node (deployable): `shared/services/surveillance-node/` (bin + `createSurveillanceNode`).
- SDK: `shared/packages/detection-client/` (`createDetectionStream` SSE · `createDetectionSocket` WS ·
  `createNodeClient` register/pull/heartbeat/publish/activate).
- Studio: `products/leumas-studio/src/admin/surveillance/` (Live/Cameras/Detections/Rules/Faces/Footage/
  Vision Lab/**Fleet**).

## Add a vision recipe (the #1 extension — zero core edits)
A recipe is one `defineRecipe({...})` in a definitions file (`recipes.builtin.js` or `domain-recipes.js`,
loaded by the `recipes.js` facade). It declares `{ id, kind, backend, configSchema, triggerProps,
renderStyle, run? }`:
- `backend: 'node'` → pure JS, runs in-process in `applyNodeRecipes` (handleDetectionPayload) over any
  detector's boxes — no model, fully testable (see `zones`, `speed`, `red-light`, `retail-interaction`).
- `backend: 'yolo-classes' | 'face' | 'python:<name>'` → runs in the Python sidecar; register the
  detector in `flask_server.py` (`register_detector('<name>', fn)`).
- `triggerProps` auto-append to the rule manifest → the filter fields appear in the Studio rule editor
  automatically (the same seam `person` used). String props = equality filters; number props = ≥ thresholds.
- A recipe emits ordinary detection entries `{ kind, class, confidence, box, ...attrs }`; the ring/SSE/
  WS/rules pass extra attrs through, so `{{plate}}`/`{{speed}}`/`{{zoneId}}` interpolate in actions.

## Multi-tenancy
Cameras carry `tenantId` (the licensed business = fleet/isolation boundary) + `siteId` + `nodeId`.
Streams/detectors/recordings/boot/SSE/WS all scope by `tenantId` (`cameraScope`/`canAccess` in the
router; `DEFAULT_TENANT='leumas'` folds legacy rows). Create via `POST /api/surveillance/cameras`
(stamps owner+tenant+site, opt-in `surveillance.maxCameras` entitlements quota).

## Edge nodes & auth
- A camera's `nodeId`: empty = the control-plane built-in node runs it; set = an external node owns it
  (the built-in ensures + boot SKIP it — no duplicate publisher). Assign in the Studio **Fleet** tab or
  via the `assignCameraToNode` adapter tool.
- Node routes (before the session gate): `POST /node/register`, `GET /node/cameras`, `POST /node/heartbeat`.
- **`requireNodeAuth`**: (1) a signed per-fleet **license** token (`@leumas/licensing`; tenant baked in,
  revocable immediately, issue at `/licensing/admin/issue` with `features:{surveillance.node:true}`),
  (2) shared `SURVEILLANCE_NODE_TOKEN` fallback, (3) loopback.
- **`/ingest`**: an `lk_` API key (`@leumas/auth verifyKey`, per-owner — one-off external cameras), OR
  the shared ingest token, OR loopback.

## Metering (PassNode — opt-in)
Per-camera `pay-per-use` (`surveillance:camera`, hard gate on create) + per-detection `token-usage`
(`surveillance:detect`, best-effort meter on ingest — NEVER drops a detection). No `passnode_rules` row
⇒ free. `GET /api/surveillance/usage` = camera count + quotes; Studio Fleet + Cameras show pricing.
The engine gained `passnode.charge({ user, target, units })` for machine-driven metering.

## Realtime
SSE `GET /api/surveillance/stream` (simple consumers) + WS `/ws/surveillance` (many-client live walls;
noServer + path-check-that-RETURNS multiplex, cookie-auth, fleet-scoped). Consume with
`createDetectionStream` / `createDetectionSocket`.

## Independence / white-label / packaging
- **Tap into someone else's host:** the edge node targets any control plane via
  `SURVEILLANCE_CONTROL_PLANE_URL` — the SDK is host-agnostic. A camera/node can publish to a customer's
  self-hosted Leumas surveillance cloud.
- **Appliance EXE** = control plane + built-in node in one process (today's behavior; no separate node
  needed for a single site). Remote nodes ship via `pnpm deploy --filter @leumas/surveillance-node`.
- **Extraction seam → `@leumas/surveillance-server`:** the control-plane surface is already a factory
  (`createSurveillanceRouter({ connector, triggers, requireAuth, entitlements, licensing, passnode })`) +
  `createSurveillanceWs({ server, authenticate })`. To ship a standalone white-label surveillance server
  (mirroring `@leumas/imperium-server`), wrap those two with a connector + auth + a thin Express app —
  no engine changes. Left as a clean seam; build on real demand.

## MCP / agent-programmable
The camera-manager adapter auto-projects every tool as an MCP tool + chatbot functioncall via the
adapter registry (`GET /mcp`, `POST /api/adapters/surveillance/<fn>`). Enterprise tools incl.
`listRecipes`, `listNodes`, `assignCameraToNode`, `simulateDetection`. Run the `expose-as-mcp` skill to
list/price it in Studio.

## Verify (no hardware needed)
Node-side recipe logic + the generic matcher are unit-testable (`applyNodeRecipes` + `matchDetections`).
End-to-end: POST synthetic detections to `/ingest` (loopback) → they flow through recipes → ring → SSE/WS
→ rules → actions (e.g. `send-webhook` a ticket). Missing ffmpeg/python ⇒ everything guards to a no-op.


---
Source: shared/services/knowledge/build-knowledge/surveillance-enterprise.md
Canonical: https://docs.leumas.tech/p/how-to/surveillance-enterprise
