Canonical topic reference for jettyd devices. Source of truth: apps/platform/src/mqtt.rs and apps/platform/src/routes/mqtt_auth.rs.
The device_key is returned during provisioning (see QuickStart) as dk_….
It is also visible in the dashboard under Device → Credentials.
All device topics are scoped to jettyd/{tenant_id}/{device_id}/… where tenant_id and device_id are UUIDs returned at provisioning time. Media upload topics use a shorter jettyd/{device_id}/media/… pattern without a tenant segment.
| Topic | Direction | QoS | Payload |
|---|---|---|---|
jettyd/{tenant_id}/{device_id}/telemetry |
device → broker | 1 |
{"readings": {"<metric>": <number|string>}}
confirmed
|
jettyd/{tenant_id}/{device_id}/command |
broker → device | 1 |
{"id": "<uuid>", "action": "<string>", "params": {…}}
confirmed
|
jettyd/{tenant_id}/{device_id}/config |
broker → device | 1 | payload format not yet confirmed in codebase TBD |
jettyd/{tenant_id}/{device_id}/config/ack |
device → broker | 1 | ack payload not yet confirmed in codebase TBD |
jettyd/{tenant_id}/{device_id}/command/response |
device → broker | 1 |
{"id": "<command_uuid>", "status": "<string>"}
confirmed
|
config and config/ack payloads are not yet confirmed in the platform source. These topics are authenticated and ACL-enforced but the payload contracts are still being finalised. The command topic payload is now confirmed — see below.
…/commandThe platform publishes a command envelope to the device. The action field determines what the device should do; command arguments are carried in params. The device ACKs via …/command/response.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"action": "reboot",
"params": {}
}
When a firmware rollout is dispatched, action is firmware_update. The params object includes the download URL, SHA-256 integrity hash, binary size, and — when the firmware was uploaded with an Ed25519 signing key configured — a detached signature over the SHA-256 digest.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"action": "firmware_update",
"params": {
"version": "1.2.0",
"url": "https://cdn.jettyd.com/firmware/abc123/firmware.bin",
"sha256": "e3b0c44298fc1c149afb…",
"size": 524288,
"signature": "a1b2c3d4…",
"signature_alg": "ed25519"
}
}
Signature verification: when signature_alg is ed25519, verify the signature (hex-encoded 64-byte Ed25519 signature) against the raw 32-byte SHA-256 digest of the downloaded binary using the tenant's public key, available from GET /v1/firmware/public-key. Skip verification if signature is absent — the firmware is integrity-checked by SHA-256 regardless. See the OTA guide for a complete implementation example.
…/telemetryDevices publish readings as a map under the readings key. Metric names are arbitrary strings; values are either numbers (stored in value_numeric) or strings (stored in value_text).
{
"readings": {
"temperature": 23.4,
"humidity": 61,
"door_state": "open"
}
}
Each key-value pair is inserted as a row in the telemetry time-series table and merged into the device's shadow.reported field.
telemetry_schema when one is set.
2xx response but no data is written. A structured WARN log is emitted containing device_id, metric, expected, and actual types.WARN level.float/number, int/integer, string, bool/boolean. The range and unit fields in the schema are not enforced.
…/command/responseAfter executing a command the device publishes a response with the command's UUID and a status string. The platform matches this to the commands table by id and marks it acknowledged.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "ok"
}
Common status values: ok, error, acked (default when omitted).
These topics implement the three-step media grant flow: device requests an upload slot, platform issues a presigned PUT URL (or quota refusal), and device confirms completion.
| Topic | Direction | QoS | Payload |
|---|---|---|---|
jettyd/{device_id}/media/request |
device → broker | 1 |
{"trigger": "interval|command|gpio", "content_type": "image/jpeg", "size_hint": <bytes>}
confirmed
|
jettyd/{device_id}/media/grant |
broker → device | 1 |
Success: {"media_id": "<uuid>", "put_url": "<presigned-url>", "expires_in_seconds": 900}Quota exceeded: {"error": "quota_exceeded", "retry_after_sec": 900}
confirmed
|
jettyd/{device_id}/media/complete |
device → broker | 1 |
{"media_id": "<uuid>", "size_bytes": <number>, "sha256": "<hex>"}
confirmed
|
…/media/requestDevice requests an upload slot. trigger must be one of interval, command, or gpio. content_type must be image/jpeg in v1. size_hint is used for quota pre-check.
{
"trigger": "interval",
"content_type": "image/jpeg",
"size_hint": 102400
}
…/media/grantPlatform responds with a presigned PUT URL (valid 900 seconds) or a quota refusal.
// Success — upload to put_url using HTTP PUT with Content-Type: image/jpeg
{
"media_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"put_url": "https://media.r2.jettyd.com/media/…?X-Amz-Signature=…",
"expires_in_seconds": 900
}
// Quota exceeded
{
"error": "quota_exceeded",
"retry_after_sec": 900
}
…/media/completeAfter uploading to R2, device notifies the platform. The platform HEAD-checks the object. If present and size matches → complete; if missing → failed. Duplicate completions on an already-complete row are idempotent.
{
"media_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"size_bytes": 102400,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
These topics are used internally by the platform but are not part of the standard device ACL.
| Topic | Direction | Notes |
|---|---|---|
jettyd/{tenant_id}/{device_id}/status |
device → broker | Online/offline heartbeat. Payload: {"status": "online"} or {"status": "offline"} |
jettyd/provision/request |
device → broker | Initial provisioning. Uses fleet token as MQTT password. Payload: {"fleet_token": "…", "device_type": "…", "firmware_version": "…", "mac_address": "…"} |
jettyd/provision/response/{device_id} |
broker → device | Returns device_id, device_key, tenant_id, and mqtt_uri for subsequent connections. |
mosquitto_pub \
--host mqtt.jettyd.com \
--port 8883 \
--tls-version tlsv1.2 \
--capath /etc/ssl/certs \
--username "$DEVICE_ID" \
--pw "$DEVICE_KEY" \
--topic "jettyd/$TENANT_ID/$DEVICE_ID/telemetry" \
--message '{"readings":{"temperature":23.4,"humidity":61}}' \
--qos 1
mosquitto_sub \
--host mqtt.jettyd.com \
--port 8883 \
--tls-version tlsv1.2 \
--capath /etc/ssl/certs \
--username "$DEVICE_ID" \
--pw "$DEVICE_KEY" \
--topic "jettyd/$TENANT_ID/$DEVICE_ID/command" \
--qos 1
DEVICE_ID, DEVICE_KEY, and TENANT_ID from the values returned at provisioning time (or from the dashboard under Device → Credentials).
The validation script at apps/docs/scripts/validate-mqtt-topics.sh checks that this page exists and contains all canonical topic patterns. Run it locally:
bash apps/docs/scripts/validate-mqtt-topics.sh
To perform a live broker smoke-test against staging, set the following environment variables and run the mosquitto_pub example above:
MQTT_HOST — staging broker hostnameDEVICE_ID, DEVICE_KEY, TENANT_ID — from a staging device