{
  "schema": "leumas.docs.page/1",
  "id": "how-to:surveillance-enterprise",
  "slug": "how-to/surveillance-enterprise",
  "kind": "pages",
  "bucket": "how-to",
  "title": "Surveillance — enterprise architecture (build knowledge)",
  "name": "Surveillance",
  "eyebrow": "build knowledge",
  "chip": null,
  "summary": "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...",
  "keywords": [
    "surveillance-enterprise",
    "millions",
    "data-plane",
    "independence white-label packaging",
    "licensed",
    "leumas surveillance enterprise",
    "how to surveillance enterprise",
    "scales"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# Surveillance — enterprise architecture (build knowledge)\n\nHow the Leumas surveillance system is built and how to extend it. It scales from a single box to\nmillions of cameras across many licensed businesses via a **control-plane / data-plane split**, and every\ndetection capability is a **plugin**. Read this before touching surveillance.\n\n## The shape\n\n```\nCONTROL PLANE  (leumas-api — touches EVENTS, not raw video)\n  tenant/fleet/site camera registry · rule engine · licensing/fleet keys · PassNode metering ·\n  entitlement quotas · live event index (SSE /api/surveillance/stream + WS /ws/surveillance) · DVR index\n        ▲ pull cameras            │ publish events + heartbeat (keyed)         ▲ live walls / rules\n        │ GET /node/cameras       ▼ POST /ingest (per-node/-device auth)       │\nDATA PLANE  (edge detection nodes — run the CV near the cameras)\n  @leumas/surveillance-node  ── reuses the SAME camera-manager services (media/detector/recorder) ──\n                             ── loads enabled VISION RECIPES per camera ── keyed publish SDK\n  (the API's in-process sidecars ARE \"the built-in node\" for a single-box/EXE install — no fork)\n```\n\n**Never haul raw video to a central GPU.** Detection runs near the cameras; only events (+ optional\nclips) reach the control plane. Edge-first is the whole scaling story.\n\n## Where things live\n- Engine (connector-free pipelines + registry): `shared/engines/adapters/surveillance/camera-manager/`\n  — `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).\n- Control-plane HTTP: `products/leumas-api/src/routes/surveillance.js`; WS: `products/leumas-api/src/ws/surveillance.js`.\n- Edge node (deployable): `shared/services/surveillance-node/` (bin + `createSurveillanceNode`).\n- SDK: `shared/packages/detection-client/` (`createDetectionStream` SSE · `createDetectionSocket` WS ·\n  `createNodeClient` register/pull/heartbeat/publish/activate).\n- Studio: `products/leumas-studio/src/admin/surveillance/` (Live/Cameras/Detections/Rules/Faces/Footage/\n  Vision Lab/**Fleet**).\n\n## Add a vision recipe (the #1 extension — zero core edits)\nA recipe is one `defineRecipe({...})` in a definitions file (`recipes.builtin.js` or `domain-recipes.js`,\nloaded by the `recipes.js` facade). It declares `{ id, kind, backend, configSchema, triggerProps,\nrenderStyle, run? }`:\n- `backend: 'node'` → pure JS, runs in-process in `applyNodeRecipes` (handleDetectionPayload) over any\n  detector's boxes — no model, fully testable (see `zones`, `speed`, `red-light`, `retail-interaction`).\n- `backend: 'yolo-classes' | 'face' | 'python:<name>'` → runs in the Python sidecar; register the\n  detector in `flask_server.py` (`register_detector('<name>', fn)`).\n- `triggerProps` auto-append to the rule manifest → the filter fields appear in the Studio rule editor\n  automatically (the same seam `person` used). String props = equality filters; number props = ≥ thresholds.\n- A recipe emits ordinary detection entries `{ kind, class, confidence, box, ...attrs }`; the ring/SSE/\n  WS/rules pass extra attrs through, so `{{plate}}`/`{{speed}}`/`{{zoneId}}` interpolate in actions.\n\n## Multi-tenancy\nCameras carry `tenantId` (the licensed business = fleet/isolation boundary) + `siteId` + `nodeId`.\nStreams/detectors/recordings/boot/SSE/WS all scope by `tenantId` (`cameraScope`/`canAccess` in the\nrouter; `DEFAULT_TENANT='leumas'` folds legacy rows). Create via `POST /api/surveillance/cameras`\n(stamps owner+tenant+site, opt-in `surveillance.maxCameras` entitlements quota).\n\n## Edge nodes & auth\n- A camera's `nodeId`: empty = the control-plane built-in node runs it; set = an external node owns it\n  (the built-in ensures + boot SKIP it — no duplicate publisher). Assign in the Studio **Fleet** tab or\n  via the `assignCameraToNode` adapter tool.\n- Node routes (before the session gate): `POST /node/register`, `GET /node/cameras`, `POST /node/heartbeat`.\n- **`requireNodeAuth`**: (1) a signed per-fleet **license** token (`@leumas/licensing`; tenant baked in,\n  revocable immediately, issue at `/licensing/admin/issue` with `features:{surveillance.node:true}`),\n  (2) shared `SURVEILLANCE_NODE_TOKEN` fallback, (3) loopback.\n- **`/ingest`**: an `lk_` API key (`@leumas/auth verifyKey`, per-owner — one-off external cameras), OR\n  the shared ingest token, OR loopback.\n\n## Metering (PassNode — opt-in)\nPer-camera `pay-per-use` (`surveillance:camera`, hard gate on create) + per-detection `token-usage`\n(`surveillance:detect`, best-effort meter on ingest — NEVER drops a detection). No `passnode_rules` row\n⇒ free. `GET /api/surveillance/usage` = camera count + quotes; Studio Fleet + Cameras show pricing.\nThe engine gained `passnode.charge({ user, target, units })` for machine-driven metering.\n\n## Realtime\nSSE `GET /api/surveillance/stream` (simple consumers) + WS `/ws/surveillance` (many-client live walls;\nnoServer + path-check-that-RETURNS multiplex, cookie-auth, fleet-scoped). Consume with\n`createDetectionStream` / `createDetectionSocket`.\n\n## Independence / white-label / packaging\n- **Tap into someone else's host:** the edge node targets any control plane via\n  `SURVEILLANCE_CONTROL_PLANE_URL` — the SDK is host-agnostic. A camera/node can publish to a customer's\n  self-hosted Leumas surveillance cloud.\n- **Appliance EXE** = control plane + built-in node in one process (today's behavior; no separate node\n  needed for a single site). Remote nodes ship via `pnpm deploy --filter @leumas/surveillance-node`.\n- **Extraction seam → `@leumas/surveillance-server`:** the control-plane surface is already a factory\n  (`createSurveillanceRouter({ connector, triggers, requireAuth, entitlements, licensing, passnode })`) +\n  `createSurveillanceWs({ server, authenticate })`. To ship a standalone white-label surveillance server\n  (mirroring `@leumas/imperium-server`), wrap those two with a connector + auth + a thin Express app —\n  no engine changes. Left as a clean seam; build on real demand.\n\n## MCP / agent-programmable\nThe camera-manager adapter auto-projects every tool as an MCP tool + chatbot functioncall via the\nadapter registry (`GET /mcp`, `POST /api/adapters/surveillance/<fn>`). Enterprise tools incl.\n`listRecipes`, `listNodes`, `assignCameraToNode`, `simulateDetection`. Run the `expose-as-mcp` skill to\nlist/price it in Studio.\n\n## Verify (no hardware needed)\nNode-side recipe logic + the generic matcher are unit-testable (`applyNodeRecipes` + `matchDetections`).\nEnd-to-end: POST synthetic detections to `/ingest` (loopback) → they flow through recipes → ring → SSE/WS\n→ rules → actions (e.g. `send-webhook` a ticket). Missing ffmpeg/python ⇒ everything guards to a no-op.\n",
  "source": {
    "path": "shared/services/knowledge/build-knowledge/surveillance-enterprise.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 7092,
    "hash": "44b38b124f7c4fd1450a4550c2fa6164e0f1e599"
  },
  "urls": {
    "html": "/p/how-to/surveillance-enterprise",
    "json": "/docs/how-to/surveillance-enterprise.json",
    "md": "/docs/how-to/surveillance-enterprise.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
