Every connected product ends up with the same backend: a list of devices, rules about who may see them, the last thing each device said, and something that shouts when a value goes wrong. I have built that backend more than once. infinimesh is the open source version I wish I had each time.
infinimesh is an Apache-2.0 IoT platform written in Go. You run it on your own servers. Version 3.4 adds rules and alarms in core, so the "something that shouts" part is no longer another system you bolt on.
This post goes from an empty laptop to a rule firing an alarm. You need Docker with Compose and about ten minutes.
The same thing in the console, the device list of a small demo factory:
1. Start the stack
git clone https://github.com/infinimesh/core.git
cd core
cp .env.example .env
.env.example defaults to BASE_DOMAIN=127.0.0.1.nip.io. I use plain localhost, so nothing depends on outside DNS. Pin the release too:
sed -i.bak -e 's/^BASE_DOMAIN=.*/BASE_DOMAIN=localhost/' \
-e 's/^INFINIMESH_VERSION=.*/INFINIMESH_VERSION=v3.4.0/' .env
docker compose up -d
The API is now on api.localhost:443 and the console on https://console.localhost. The admin login is infinimesh / infinimesh. Change it before this machine sees a network you don't trust.
One note: port 8000 is not the CLI port and returns 404. Use 443.
2. Install the inf CLI
With Homebrew on macOS or Linux:
brew tap infinimesh/inf-cli https://github.com/infinimesh/inf-cli
brew install inf
If brew says the tap is untrusted, run brew trust infinimesh/inf-cli once and install again.
No Homebrew? Download the archive for your system from the inf-cli releases page, unpack it and put inf on your PATH.
Check you have 3.4. The 3.2 CLI cannot talk to a 3.4 core:
inf version
3. Log in
inf login api.localhost:443 infinimesh infinimesh --insecure-skip-verify
inf whoami
The local Traefik certificate does not cover api.localhost, so a local login needs --insecure-skip-verify. On a real domain with a real certificate, drop it.
4. Create a namespace
A namespace is where devices, rules and quotas live. Quotas are flags here, and zero means no limit:
inf ns create --title plant-a --max-devices 50 --device-mps 5
It prints the new namespace:
UUID: 305b9f5f-43e5-487b-989f-0cae77fd30fc
Title: plant-a
Quota: devices=50 device-mps=5 namespace-mps=0
That UUID is <namespace> below. inf ns list shows it again if you lose it.
5. Create a device
A device template is YAML or JSON with title, enabled and tags. Save this as pump.yaml:
title: pump-01
enabled: true
tags:
- pump
The device authenticates with a client certificate. For a test, a self-signed one is enough. Keep the key, the device needs it later for MQTT:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout pump.key -out pump.pem -days 365 -subj "/CN=pump-01"
Then create it:
inf devices create pump.yaml --crt pump.pem --namespace <namespace>
Template {"enabled":true,"tags":["pump"],"title":"pump-01"}
Device Created, UUID: e3ab261b-d1b4-4a95-b9d5-7c31df508f49
That UUID is <device> from here on.
6. Write a rule
A rule is a CEL expression on device state. A reading stored at reported.data.temp is just reported.temp in a rule. Test the expression first:
inf rules dry-run --when 'reported.temp > 70' --clear 'reported.temp < 60'
Then save it:
inf rules create --namespace <namespace> --name overheat \
--when 'reported.temp > 70' --clear 'reported.temp < 60' --cooldown 10m
It prints the rule UUID first, then the rule:
9e87e99b-cbbb-49ee-9317-0f8bc96cc88c 305b9f5f-43e5-487b-989f-0cae77fd30fc overheat
when: reported.temp > 70
clear: reported.temp < 60
cooldown: 10m
That first UUID is <rule>. inf rules list --namespace <namespace> shows it too.
--clear is when the alarm clears by itself. --cooldown is the shortest wait before the same alarm can be raised again, so a sensor bouncing around 70 does not page you every second.
7. Report a reading
Mint a device token that may post reported state, and report a normal value:
inf devices token <device> --allow-post > device.token
inf devices state <device> --token "$(cat device.token)" --report '{"temp":21.5}'
Nothing happens, which is correct. Now make it run hot:
inf devices state <device> --token "$(cat device.token)" --report '{"temp":75}'
A real device doesn't use the CLI. Over HTTP it posts the same JSON to /v1/ingest with its device token. A 204 means it landed:
curl -sk -X POST https://api.localhost/v1/ingest \
-H "Authorization: Bearer $(cat device.token)" \
-H 'Content-Type: application/json' \
-d '{"temp":75}'
Over MQTT it connects to port 8883 with its certificate and key and publishes to devices/<device>/state/reported. A quick test with Python and pip install paho-mqtt:
import ssl
import paho.mqtt.client as mqtt
DEVICE = "<device>"
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE)
c.tls_set(certfile="pump.pem", keyfile="pump.key", cert_reqs=ssl.CERT_NONE) # local self-signed server cert only
c.connect("localhost", 8883)
c.loop_start()
c.publish(f"devices/{DEVICE}/state/reported", '{"temp":75}', qos=1).wait_for_publish(5)
c.disconnect()
The docs examples have ESP32 and Pico W code.
8. See the alarm
inf alarms list --namespace <namespace>
| RULE | DEVICE | TITLE | STATE |
| 9e87e99b-cbbb-49ee-9317-0f8bc96cc88c | e3ab261b-d1b4-4a95-b9d5-7c31df508f49 | overheat | RAISED |
There it is, raised, titled overheat. The RULE and DEVICE columns are the ids for the next two commands. It also shows up on the "Rules and Alarms" page in the console.
Acknowledge it, so your colleagues know someone is on it:
inf alarms ack --rule <rule> --device <device>
Report a cool value and it clears on its own through the clear condition. Or clear it by hand:
inf alarms clear --rule <rule> --device <device>
ack and clear take --rule and --device flags, not positional arguments. To see acked and cleared alarms too:
inf alarms list --namespace <namespace> --state any
Every ack and clear lands in the audit log:
inf audit --namespace <namespace>
That's the whole loop. Device, state, rule, alarm, ack, audit. On your laptop, under Apache-2.0.
In the console, the "Rules and Alarms" page shows the same alarm, with ack and clear buttons:
Bonus: point a firmware target at the device
infinimesh does not ship firmware. hawkBit and Mender already do that well. What 3.4 adds is a contract in the device shadow: the target version goes in desired.ota, and the device reports its progress in reported.ota.
OTA is the one addon kind that registers with core, because it touches the device OS. Every other addon just uses the API. The order matters.
Log in as root. Only root may create plugins:
inf login --root api.localhost:443 infinimesh infinimesh --insecure-skip-verify
Register an OTA addon. The token is printed once and never again, so catch it:
inf plugins create --title hawkbit --kind ota --provider hawkbit --namespace <namespace> | tee plugin.out
export ADDON_TOKEN=$(awk '/shown once/ {print $NF}' plugin.out)
The output looks like this:
cc02bcfa-57a0-4945-8a81-c474ac0506a0 OTA hawkbit public=false
provider: hawkbit
token (shown once): eyJhbGciO...
Move the token to your secret manager and delete plugin.out.
Switch OTA on for the namespace. Before the addon exists, this is refused on purpose:
inf ns update <namespace> --ota --provider hawkbit
Assign a version as the addon:
inf --token "$ADDON_TOKEN" ota assign --device <device> --version 1.2.3
updated 1
| DEVICE | ERROR |
| e3ab261b-d1b4-4a95-b9d5-7c31df508f49 | |
inf devices state <device> now shows desired {"ota":{"updated_at":"...","version":"1.2.3"}}. Your device code downloads, installs, and reports back in reported.ota. Swap --device for --namespace or --tag to roll out to many devices at once.
One thing to know: ota assign is a bulk call. It returns success as a whole and a result per device. A refused device is an error in its own row, not a failed command. Read the rows.
Clean up
To remove only what you made, delete in this order:
inf plugins delete <plugin>
inf rules delete <rule>
inf devices delete <device>
inf ns delete <namespace>
To throw the whole stack away, run docker compose down -v in the core folder. -v deletes the volumes too, so all data goes with it.
Where to go next
- Code: github.com/infinimesh/core. A star helps other people find it.
- CLI guide: Getting started with the inf CLI
- Release notes: v3.4.0
- The full story behind 3.4: https://www.novatechflow.com/2026/10/infinimesh-34-device-layer.html
- Project site: infinimesh.org
Coming soon: hawkBit and Mender addons, then an MCP server and an LLM addon, scoped by the same permissions as people. If the quickstart breaks on your machine, that's a bug and I want to hear about it in GitHub issues.



Top comments (0)