Surveillance
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...
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 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/(createDetectionStreamSSE ·createDetectionSocketWS ·
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 inapplyNodeRecipes(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)).
triggerPropsauto-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: anlk_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.