Skip to main content
Version: 3.0 (next)

MQTT Write Endpoint

External MQTT clients — sensors, PLCs, SCADA gateways — can publish data into the Unified Namespace. The rule that makes this safe: reading the UNS is a projection; writing the UNS is a function call. Devices never write data-node topics directly. Instead, every device publishes into its own inbox, and MaestroHub admits the value through the same validation chain the REST API uses — schema, permissions, quality, alerts, history. The broker's data-node topics carry only admitted values, so every MQTT subscriber sees exactly what the catalog stores.

info

The write endpoint ships with UNS and is enabled by default (ingress.mqttWriteEnabled). Disabling it leaves the topic tree write-protected — external clients still cannot publish to data nodes — but nothing consumes the inbox.

The inbox grammar​

To write a signal, publish to its own path under your inbox:

<version>/<org-slug>/_ingress/<your-client-id>/<topic-path>

For example, device press-07 writing enterprise/site/temp in org acme:

topic:    mHv1.0/acme/_ingress/press-07/enterprise/site/temp
payload: 23.5

The inbox segment must equal your MQTT client id — the broker enforces it, which is what makes per-device attribution trustworthy. Publishing to another device's inbox, or to any data-node topic, is refused. The retain flag is ignored on inbox publishes (a retained request would replay as a fresh publish forever).

Payloads are raw values or a JSON envelope, detected by shape:

raw:      23.5   ·   true   ·   RUNNING
envelope: {"value": 23.5,
"timestamp": "2026-08-22T10:00:00Z", // optional, RFC3339
"quality": 0, // optional: 0 Good · 1 Uncertain · 2 Bad
"requestId": "req-42"} // optional, echoed in the reply

A JSON object without a value key is itself the published value — structured payloads are legal. Payloads over 1 MiB are refused before parsing.

Identity and permissions​

The data steward's existing topic grants govern every write — there is nothing MQTT-specific to configure on topics. The write is attributed to a principal, and that principal needs topic:publish on the canonical path (never on _ingress paths, which are transport, not data):

Broker configurationWho the write runs as
Embedded brokerThe device authenticates at CONNECT as its own API client — the client ID as MQTT username and the client secret as password, or an OAuth2 access token from that client as password. The write runs as that client: principal, with its own grants.
EMQX with delegation (its HTTP authentication and authorization pointed at MaestroHub)Same as embedded: the same credentials, the same principal, the same grants.
EMQX without delegationThe write runs as the bridge principal (client:broker-ingress by default, ingress.bridgeSubject to change). Grant it topic:publish where MQTT-ingested data may land — without a grant, ingest is refused. The device is still named in each record's provenance.

Auto-creating topics through the endpoint additionally requires topic:create — a publish-only device cannot invent topics.

Replies: how a device learns its verdict​

MQTT has no error responses, so the endpoint answers asynchronously. Every rejected publish is published as a JSON verdict to the device's own error topic:

mHv1.0/<org-slug>/_errors/<your-client-id>

{"status": "rejected", "topic": "…", "reason": "publish_denied",
"remediation": "ask your administrator to grant topic:publish on …",
"requestId": "req-42"}

Subscribe to your own _errors topic to hear rejects (works on MQTT 3.1.1). MQTT 5 clients can additionally set a Response Topic on the publish — accepted and rejected verdicts are delivered there, provided it points inside your own _errors subtree (any other destination is ignored). On the embedded broker, MQTT 5 clients also receive a Not Authorized reason code directly in the PUBACK for topic-level refusals.

Reject reasons are stable strings: publish_denied, autocreate_denied, schema_type_violation, foreign_org_topic, malformed_payload, rate_limited, retained_request, unknown_org.

Operating the endpoint​

  • The reject feed — Alerts → Ingress rejects lists every refusal, coalesced per device + topic + reason with a running count. GET /api/v1/uns/ingress/rejects serves the same rows. Requires the uns_ingress_rejects:read permission.
  • The attention card — a sustained reject rate (≥ 1/min per org) raises a warning on the overview, linking to the feed.
  • Metrics — uns_ingress_accepted_total and uns_ingress_rejected_total{reason} in the monitoring stack.
  • Rate limits — ingress.perClientRatePerSec (default 50) and ingress.perOrgRatePerSec (default 2000) in config.yaml; 0 disables a limit.

Substrate trespass (external brokers)​

On a customer-managed broker, MaestroHub cannot physically block a client from publishing to data-node topics — that is the broker's job (below). What it does instead: any data-tree publish that did not come from MaestroHub itself is flagged in the reject feed as substrate_trespass and is never admitted to the catalog. If you see trespass rows, a producer is bypassing the endpoint — check the broker's delegation and point the producer at its inbox.

External broker: EMQX​

EMQX is the only supported external broker. Configure it to delegate authentication and authorization to MaestroHub — POST /api/v1/authz/mqtt/authenticate for each CONNECT and POST /api/v1/uns/mqtt/acl for each publish and subscribe. MaestroHub then enforces the inbox rule and every topic grant itself, exactly as on the embedded broker, so the broker needs no ACL file of its own.

The MQTT Broker page generates the EMQX configuration for this, and its Check enforcement button confirms the broker obeys MaestroHub's decisions. See EMQX (external).