Python code snippets and a docker environment are linked at the end of the post!!
We’ll wire up a digital twin for a tiny device called dt-led and a logical
controller called dt-controller. The controller tells the LED device: turn ON/OFF.
The device acknowledges and updates its twin state (the “source of truth” in Ditto).
- Command path: Controller → Ditto (HTTP inbox message) → Ditto publishes to MQTT → Device subscribes and acts
- State path: Device → Ditto (via HTTP or MQTT) → Ditto persists twin → Ditto emits twin events (optional MQTT)
You’ll meet four core Ditto ideas along the way:
- Policies — Who can do what on which resources (things, messages, policies).
- Things — Your twins (
dt-ledanddt-controller) with attributes and features. - Connections — How Ditto bridges to external systems (here: HiveMQ MQTT broker).
- Ditto Protocol — The JSON envelope Ditto uses on MQTT for commands/events.
The high‑level architecture
Key takeaways:
- Ditto does not “poll” devices. Devices either subscribe (MQTT) and react to commands, or push twin updates.
- MQTT topics you own (like
devices/<thing:id>) carry Ditto Protocol JSON envelopes when Ditto’sdittopayload mapping is enabled. - Everything is allowed/blocked by Policy, so we’ll grant just enough privileges for the connection and a default app subject.
Ditto Essentials in 90 seconds
-
A Thing has an ID like
org.eclipse.ditto:dt-led.
It can carry attributes (metadata) and features (capabilities + state).
We’ll use featurestatus_ledwith property{ "status": "ON" | "OFF" }. -
A Policy binds subjects (identities) to resources with
READ/WRITEgrants.
Two subjects matter here:nginx:ditto(a basic‑auth app user hitting the REST API through Ditto’s HTTP endpoint).connection:hivemq-mqtt(the MQTT connection itself, so Ditto can read/write using it).
-
A Connection of type
mqtttells Ditto which MQTT addresses (topics) to consume (sources) and where to publish (targets).
We’ll use thedittopayloadMapping so messages are JSON envelopes that Ditto understands.
Identity & Policy — unlocking the right doors
Our policy grants just-enough permissions to the two subjects we care about:
- Owner / app subject:
nginx:ditto→ mayREAD/WRITEthing:/,policy:/,message:/ - Connection subject:
connection:hivemq-mqtt→ mayREAD/WRITEthing:/,message:/
Here is a minimal, friendly version of the policy document you’d PUT to Ditto:
{
"entries": {
"owner": {
"subjects": { "nginx:ditto": { "type": "nginx basic auth user" } },
"resources": {
"thing:/": { "grant": ["READ", "WRITE"], "revoke": [] },
"policy:/": { "grant": ["READ", "WRITE"], "revoke": [] },
"message:/": { "grant": ["READ", "WRITE"], "revoke": [] }
}
},
"connection": {
"subjects": {
"connection:hivemq-mqtt": { "type": "Connection to HiveMQ MQTT broker" }
},
"resources": {
"thing:/": { "grant": ["READ", "WRITE"], "revoke": [] },
"message:/": { "grant": ["READ", "WRITE"], "revoke": [] }
}
}
}
}
Why give the connection
thing:/+message:/? - So Ditto is allowed to publish live messages to devices, and consume device updates to modify the twin.
Modeling the twins (Things)
We’ll create two things:
org.eclipse.ditto:dt-ledattributes.name = "LED Device"features.status_led.properties.status = "OFF"(initial state)
org.eclipse.ditto:dt-controllerattributes.name = "LED Controller"
Conceptually, the LED thing holds state. The Controller is stateless and just sends commands.
You’d upsert each Thing with an HTTP
PUTto/api/2/things/<thingId>, including thepolicyIdthat points at the policy we created.
The MQTT connection (Ditto ↔ HiveMQ)
Connection settings worth remembering:
id:hivemq-mqttconnectionType:mqttconnectionStatus:open(Ditto will connect immediately)uri:tcp://hivemq:1883(inside Docker this might be the service name; on bare metal, use your host/IP)payloadMappings:[{{"mappingId":"ditto"}}](so payloads are Ditto Protocol JSON envelopes)authorizationContext:[ "connection:hivemq-mqtt" ](so the policy entry applies)
Sources (consuming from device → Ditto)
{
"sources": [
{
"addresses": ["devices/#"],
"authorizationContext": ["connection:hivemq-mqtt"],
"qos": 1,
"payloadMappings": [{ "mappingId": "ditto" }]
}
]
}
Any publish to
devices/...(e.g.,devices/org.eclipse.ditto:dt-led/twin) will be ingested by Ditto and interpreted as a Ditto Protocol message.
Targets (publishing from Ditto → device)
{
"targets": [
{
"address": "devices/{{ thing:id }}",
"topics": ["_/_/things/live/messages", "_/_/things/twin/events"],
"authorizationContext": ["connection:hivemq-mqtt"],
"qos": 1,
"payloadMappings": [{ "mappingId": "ditto" }]
}
]
}
- When Ditto routes a live message to a device, it publishes a Ditto Protocol envelope to the MQTT topic:
devices/<thing:id>(for example:devices/org.eclipse.ditto:dt-led). - When a twin event occurs, Ditto can also emit it to the same base address (using the configured
topics).
The message flow - end‑to‑end
Controller → LED (HTTP inbox message)
The controller tells the LED to toggle via Ditto’s HTTP inbox:
curl -u ditto:ditto -H "Content-Type: application/json" -X POST 'http://localhost:8080/api/2/things/org.eclipse.ditto:dt-led/inbox/messages/toggle_led?timeout=0' -d '{"status":"ON","source_thing":"org.eclipse.ditto:dt-controller"}'
Ditto routes this as a live message to MQTT target devices/org.eclipse.ditto:dt-led. The payload (what the device sees) is a Ditto Protocol envelope like:
{
"topic": "org.eclipse.ditto/dt-led/things/live/messages/toggle_led",
"headers": {
"content-type": "application/json",
"correlation-id": "..."
},
"path": "/",
"value": { "status": "ON", "source_thing": "org.eclipse.ditto:dt-controller" }
}
Note the internal
topicis a Ditto Protocol topic (with/after the namespace), while the MQTT broker topic you subscribe to isdevices/org.eclipse.ditto:dt-led.
LED device → Ditto (twin update)
Once the device sets its real LED, it reports state back to the twin. Two equivalent options:
A) HTTP to the twin’s feature path
curl -u ditto:ditto -H "Content-Type: application/json" -X PUT 'http://localhost:8080/api/2/things/org.eclipse.ditto:dt-led/features/status_led/properties' -d '{"status":"ON"}'
B) MQTT publish (Ditto Protocol)
- Publish to broker topic:
devices/org.eclipse.ditto:dt-led/twin - Payload (Ditto Protocol envelope telling Ditto to modify the twin):
{
"topic": "org.eclipse.ditto/dt-led/things/twin/commands/modify",
"headers": {
"response-required": false,
"content-type": "application/vnd.eclipse.ditto+json",
"correlation-id": "led-{timestamp}"
},
"path": "/features/status_led/properties",
"value": { "status": "ON" }
}
Ditto applies the update; anyone watching the twin gets events. With our connection’s targets.topics set to include twin/events, Ditto can also publish those events out to MQTT for downstream consumers or dashboards.
Observability & sanity checks
- Check the connection:
GET /api/2/connections/hivemq-mqtt→ status should be OPEN and connected. - Watch the twin:
GET /api/2/things/org.eclipse.ditto:dt-led→ featurestatus_led.properties.statusshould reflect the latest value. - MQTT introspection: Use a generic MQTT client to subscribe to
devices/#and see messages coming/going.
Troubleshooting
| Symptom | Likely Cause | What to check |
|---|---|---|
401 Unauthorized on HTTP | Wrong basic‑auth | Use ditto:ditto (demo) or your creds; confirm you hit the correct /api/2 base path |
403 from Ditto | Policy denies the action | Ensure policy grants READ/WRITE on thing:/ and message:/ for both nginx:ditto and connection:hivemq-mqtt |
Ditto connection stuck CLOSED | Broker not reachable or wrong uri | Is the broker at tcp://hivemq:1883 (inside Docker) or tcp://localhost:1883 (host)? Firewall? |
| Device doesn’t get commands | Wrong MQTT topic | Device must subscribe to devices/{{ thing:id }} (e.g., devices/org.eclipse.ditto:dt-led) |
| Device publishes but Ditto ignores it | Missing ditto payload mapping | Ensure both sources and targets include payloadMappings: [{{"mappingId":"ditto"}}] |
| Twin doesn’t change | Wrong Ditto Protocol path | For our LED: /features/status_led/properties is the path to modify |
| No twin events on MQTT | Targets topics missing twin/events | Add "_/_/things/twin/events" to the connection targets.topics |
Production hardening - beyond the demo
- TLS for MQTT (
ssl://...) and mutual auth between broker and Ditto. - Use fine‑grained Policies (avoid global
thing:/grants; scope to your things). - Rotate secrets; avoid shipping the
ditto:dittodemo user. - Define consistent topic conventions (
devices/<thing:id>[/...]) and keep them stable. - Validate payload schemas for your business domain (e.g., restrict
statustoON|OFF). - Add observability (Ditto connection metrics, broker metrics, dead‑letter topics for bad envelopes).
Where to go next
- Replace
status_ledwith your real features (sensors/actuators). - Add desiredProperties to let cloud apps request future state and have devices converge.
- Use policies per device for least privilege.
- Emit twin events to analytics pipelines (via the same MQTT bridge or Kafka).
Code Snippets
Add the following HiveMQ service to the Docker Compose file in the Ditto repository on GitHub:
# HiveMQ MQTT broker (Community Edition)
hivemq:
image: hivemq/hivemq-ce:latest
deploy:
resources:
limits:
memory: 512m
networks:
default:
aliases: [hivemq]
ports:
# - 8181:8080
- 1883:1883 # MQTT
logging:
options: { max-size: 50m }
# If you prefer HiveMQ Enterprise Trial with Control Center UI, use:
# image: hivemq/hivemq4:latest
# ports:
# - "1883:1883" # MQTT
# - "8082:8080" # Control Center UI available at http://localhost:8082docker-compose.yaml
Python examples are available in this GitHub Gist https://gist.github.com/anshdavid/5092b13b74fa8fb9827b9328f5013814
If this post saved you a few hours, share it or drop a ⭐ on your internal knowledge base!