{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/surveillance-node",
  "slug": "services/surveillance-node",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/surveillance-node — Surveillance edge node (data plane)",
  "name": "@leumas/surveillance-node",
  "eyebrow": "Surveillance edge node (data plane)",
  "chip": null,
  "summary": "Leumas Surveillance edge node — the data plane. Runs the SAME camera-manager media/detector/recorder pipelines near the cameras, pulls its assigned cameras from a control plane over HTTP, and...",
  "keywords": [
    "surveillance-node",
    "heartbeats",
    "camera-manager",
    "detector",
    "recorder",
    "surveillance node api",
    "leumas surveillance node",
    "pipelines"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/surveillance-node — Surveillance edge node (data plane)\n\nRuns the Leumas camera-manager pipelines **near the cameras** and publishes only detection **events** to\na control plane (the Leumas API) — never raw video. This is the horizontal-scale half of the\ncontrol/data-plane split: each node handles its local cameras; the control plane holds the registry,\nrules, metering, and event index.\n\nIt is **not a fork** — it imports the identical `@leumas/adapter-surveillance` pipeline services\n(`streams`/`media`/`detector`/`recorder`, which are connector-free) and orchestrates them against\ncameras it pulls over HTTP. The API's own in-process sidecars are the \"built-in node\" for a single-box /\nEXE install, so small deployments need no separate node at all.\n\n## Run\n\n```bash\nSURVEILLANCE_CONTROL_PLANE_URL=https://studio.example.com \\\nSURVEILLANCE_NODE_TOKEN=<shared node token> \\\nSURVEILLANCE_INGEST_TOKEN=<shared ingest token> \\\nSURVEILLANCE_NODE_TENANT=<tenant slug> \\\nSURVEILLANCE_NODE_ID=lobby-box-1 \\\nnode bin/leumas-surveillance-node.js\n```\n\nAssign cameras to a node by setting each camera's `nodeId` to the node's `SURVEILLANCE_NODE_ID`\n(cameras with no `nodeId` stay on the control-plane built-in node). The node:\n\n1. **registers** (`POST /api/surveillance/node/register`),\n2. **pulls** its assigned cameras (`GET /api/surveillance/node/cameras`),\n3. runs `ensureStreams` + `ensureDetections` + `reconcileRecording` locally (the local sidecar posts\n   detections to the control plane's `/ingest`), and\n4. **heartbeats** (`POST /api/surveillance/node/heartbeat`) — all on a periodic reconcile loop.\n\n## Deploy\n\nShip it self-contained with `pnpm deploy --filter @leumas/surveillance-node <dir>` (same discipline as\nthe API EXE). It needs `ffmpeg` + `mediamtx` + the Python detection extras on the box to actually run\ndetection; without them it registers, pulls, and heartbeats (wiring stays verifiable), and detection is\na clean no-op.\n\n## Auth\n\nPhase 4 uses a shared `SURVEILLANCE_NODE_TOKEN` (loopback-only if unset) — fine for a single-tenant\nappliance. Phase 5 upgrades node auth to signed, revocable **per-fleet licenses** (`@leumas/licensing`)\nfor safe multi-tenant cloud, without changing these routes or this client.\n",
  "source": {
    "path": "shared/services/surveillance-node/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 2436,
    "hash": "ad8d34700dda7a466b3dc2ecdfb91850ecc1ce57"
  },
  "urls": {
    "html": "/p/services/surveillance-node",
    "json": "/docs/services/surveillance-node.json",
    "md": "/docs/services/surveillance-node.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [
      "how-to:surveillance-enterprise"
    ],
    "skills": []
  },
  "exports": null
}
