An NMOS IS-04 registry: Registration API and Query API. Persistence targets ScyllaDB via the Cassandra protocol (cassandra-driver).
mDNS is out of scope for this codebase.
For DNS-SD follow the guide available in AMWA INFO-004 in order to set up the correct entries in your preferred DNS server.
Multiple registry processes can run behind a load balancer. They do not share memory; they share one Scylla keyspace. Any instance can serve Registration or Query HTTP traffic; all instances observe the same data after Scylla replicates writes.
This matches IS-04’s guidance that several Registration or Query API instances may sit in front of a common registry (Load balancing & redundancy).
- Fastify for HTTP performance and clear plugin boundaries.
@fastify/websocketfor subscription transports.- TypeScript, ESM, Node 22+.
flowchart LR
subgraph clients [Clients]
Node[NMOS Node]
Controller[Controller / Query client]
end
subgraph any_instance [Any registry instance]
Reg[Registration routes]
Query[Query routes]
Subs[Subscription manager]
Poller[Change poller]
WS["/ws WebSocket"]
end
Scylla[(ScyllaDB)]
Node -->|POST /resource etc.| Reg
Controller -->|GET query + POST subscriptions| Query
Query -->|ws_href| Controller
Controller -->|uid| WS
Reg --> Scylla
Query --> Scylla
Subs --> Scylla
Poller -->|read change_log| Scylla
Poller -->|dispatch| Subs
Subs -->|grains| WS
- Node.js 22+
- ScyllaDB (or Cassandra) reachable on the CQL port (default 9042)
npm install
npm run buildArtifacts go to dist/. Run compiled output with:
npm startnpm run devnpm run lintnpm testVitest runs all src/**/*.test.ts files.
npm run test:watchkeeps Vitest in watch mode during development.
The repo includes a minimal Compose file:
EXTERNAL_HOST=YOUR_EXTERNAL_IP docker compose up -d --buildor you run the one with the load balancer
EXTERNAL_HOST=192.100.200.1 LB_PORT=8082 HEARTBEAT_GC_INTERVAL_SECONDS=12 docker compose -f docker-compose-cluster.yml up --buildand shutdown
docker compose -f docker-compose-cluster.yml down --timeout 90 --remove-orphansWait until Scylla accepts CQL on 9042, then start the registry.
| Variable | Default | Purpose |
|---|---|---|
HOST |
0.0.0.0 |
HTTP listen address |
PORT |
8080 |
HTTP listen port |
NMOS_API_VERSIONS |
v1.2,v1.3 |
Comma-separated path versions (e.g. v1.2,1.3). Each value is normalised (normalizeApiPathVersion in config.ts), deduped, sorted, and mounted as /x-nmos/registration/<v>/… and /x-nmos/query/<v>/…. |
PUBLIC_HTTP_BASE |
http://127.0.0.1:{PORT} |
Public HTTP URL for your deployment (loaded into config; use your LB URL in HA setups) |
PUBLIC_WS_BASE |
ws://127.0.0.1:{PORT} |
Base used when building subscription ws_href (set to wss://… behind TLS termination) |
QUERY_API_SOURCE_ID |
(see config.ts) |
UUID for source_id in WebSocket grains |
SCYLLA_CONTACT_POINTS |
127.0.0.1 |
Comma-separated CQL hosts |
SCYLLA_LOCAL_DC |
datacenter1 |
Local datacenter name (must match cluster topology) |
SCYLLA_KEYSPACE |
nmos_registry |
Keyspace name |
SCYLLA_REPLICATION_FACTOR |
1 |
Per-datacenter RF for NetworkTopologyStrategy |
CHANGE_POLL_MS |
200 |
How often each instance polls change_log |
CHANGE_LOG_TTL_SECONDS |
604800 (7d) |
TTL applied to change_log rows |
PERSISTED_SUBSCRIPTION_TTL_SECONDS |
86400 (24h) |
TTL for persisted_subscriptions rows (refreshed periodically while sockets are connected) |
HEARTBEAT_GC_INTERVAL_SECONDS |
12 |
Heartbeat garbage collection interval (IS-04 default) |
LOG_LEVEL |
info |
Logging level (e.g., info, debug, warn, error) |
load-test/scalability.js is a plain Node.js scalability test — no external test framework required. Each node is simulated by an independent async task with its own setInterval timer that fires exactly HEARTBEAT_INTERVAL_S seconds after the node's registration completes, then every HEARTBEAT_INTERVAL_S seconds thereafter.
Node.js (uses built-in fetch and crypto.randomUUID). No npm install needed.
node load-test/scalability.jsOverride defaults with environment variables:
BASE_URL=http://127.0.0.1:8080 NODE_COUNT=10000 SPAWN_RATE=100 \
node load-test/scalability.js| Variable | Default | Description |
|---|---|---|
BASE_URL |
http://127.0.0.1:8080 |
Target registry URL |
NODE_COUNT |
1000 |
Total number of nodes to simulate |
SPAWN_RATE |
50 |
Nodes spawned per second during ramp-up |
HEARTBEAT_INTERVAL_S |
5 |
Seconds between heartbeats per node (IS-04 spec) |
RUN_DURATION_S |
120 |
Seconds to sustain heartbeats after all nodes are registered |
API_VERSION |
v1.3 |
NMOS Registration API version |
- Each node is an independent async task: it registers once, then schedules a
setIntervalthat fires exactlyHEARTBEAT_INTERVAL_Sseconds after registration - Nodes are spawned at
SPAWN_RATE/s so heartbeats are naturally staggered across the population - A 404 heartbeat response triggers automatic re-registration
- Stats are printed every 10 seconds showing registration and heartbeat counts and error rate
- The process exits with code 1 if the heartbeat error rate exceeds 1%