Skip to content

Messaging

Partial

This page is the one home of the two-lane data plane: the admission / trusted / persistence split, the CDC publisher, and the ingest-path enumeration live here, only named (with a link) everywhere else.

Omniglass has two typed contracts: the public API (HTTP and OpenAPI) and this sibling, the internal and edge transport, a NATS subject contract over JetStream carrying service-to-service traffic, the edge, and the live UI. Postgres stays the system of record; NATS moves. Deployment topology: scaling.

Three ingest paths exist today, and this list is the enumeration’s one home:

  • The node bus path. A node publishes a TelemetryBatch on its own subject (og.v1.telemetry.<node>), and the server binds each sample’s owner from the task’s interface (collection).
  • The API push path. POST /telemetry:push is the first-party HTTP ingest write: a scoped caller declares the owner and the route’s scope check is the fence. The API publishes the batch onto the bus (og.v1.api.telemetry, trusted by subject, below) rather than writing Postgres directly, so pushed records are visible to the same stream consumers and land in history the same way (API).
  • The raw log path. A raw log line rides either transport in the same batch but lands on its own untyped lane, log_line: no property name, no registry gate (ADR-0066).

The two transports meet at one land() write path, so neither can drift from the other’s semantics; the mechanics of land() (reject-not-project, the transition-only state guard, the current-value derive) belong to samples.

As built today there is one stream, OG_TELEMETRY, bound to the node subjects (og.v1.telemetry.*) and the API push subject (og.v1.api.telemetry), consumed by the single durable og-telemetry-worker; og.v1.telemetry.<node> is the sample firehose itself, carrying each node’s samples and its raw self-logs. The set below is the target topology:

The edge stamps ts, so the system is ts-authoritative and needs no strict ordering on the wire. Today’s delivery contract is weaker than the target: node publishes are fire-and-forget core NATS (no publish ack, no Nats-Msg-Id) and the consumer acks once after multi-transaction writes, so delivery is at-least-once with a known duplicate risk until #430 lands (see #430 and #311).

Subjects are hierarchical and scope is expressed in them, not bolted on:

  • The API telemetry lane (og.v1.api.telemetry) is trusted by subject. A first-party push (POST /telemetry:push) is authorized at the route, so the API publishes as a trusted server producer with no admission pass, and the ingest consumer believes the owner the batch carries because of the subject it arrived on, never because the field is populated. A batch on og.v1.telemetry.* that asserts an owner is dropped. Only the server’s own credential can reach this subject: a node’s grant is an explicit allow-list of its own three subjects, and the lane sits outside the single-token og.v1.telemetry.* wildcard.
  • Subject permissions gate the subject string. A node may publish and subscribe only the subjects for its placement; the grant is mechanically derived from placement, a coarse transport gate, not a second copy of the ABAC model. Operators never connect to the bus (see the live UI relay below).

Every control-plane subject is og.v1.<verb>.<node> (ADR-0081): the node name is the last token, exactly one token, which is what lets the server subscribe per-verb single-token wildcards (og.v1.worklist.*) and lets a node’s credential be an explicit allow-list of its own subjects plus its private reply namespace (_INBOX.<node>). The verb family today is worklist (request-reply), heartbeat, and telemetry (the JetStream firehose); worklist-changed is reserved for the server’s re-pull nudge, and og.v1.command.<node> is the committed future per-node command queue. The trusted push lane og.v1.api.telemetry deliberately sits in its own segment rather than as a reserved node name under og.v1.telemetry.* (above): structural impossibility beats a naming convention.

Addressing is node-anchored and batch-granular: a record has no subject of its own, and its name is payload, not topic. Per-record subjects (the MQTT-style og.v1.telemetry.<node>.<component>.<metric> tree) were rejected: the consumer needs the whole batch anyway, a subject per record explodes the permission grammar without adding a fence (the admission consumer owns the owner fence), and the one-token name rule was never justified by names as topic tokens. One consequence is that the core-NATS verbs’ server-side consumers (worklist, heartbeat) are singletons by construction; the HA fork for them (queue groups versus worklist reassignment) is named and deferred (scaling). Telemetry does not face that fork: its ingest is a named durable JetStream consumer, and a second server attaching the same durable joins it and splits deliveries.

Synchronous internal calls use NATS request-reply: an in-process call in single-binary mode, a request over the bus when modes split across pods. The public API never uses request-reply (it is HTTP); request-reply is the east-west wire only.

Related: API, scaling, nodes (the edge as a NATS client), workers (the JetStream consumers), and storage.