Quickstart
Run the dashboard server on your computer, read a dashboard, save a new one, and see the server refuse a request it should refuse. It takes about five minutes.
You need Node.js 22.13 or later. Nothing else: the server stores data in a SQLite file that Node already supports.
Start the server
MIOT_DASHBOARD_INSECURE_AUTH=true \
MIOT_DASHBOARD_SEED=example \
MIOT_DASHBOARD_STORE=sqlite \
npx @microboxlabs/miot-dashboard-serverThe server starts on http://127.0.0.1:3070 and writes its database to ./data. Its first log lines say how it is configured:
{"level":"info","msg":"store","store":"sqlite at ./data/dashboards.db, documents inline"}
{"level":"info","msg":"identity","auth":"unverified request headers (local use only)"}
{"level":"info","msg":"tenants","entitlement":"the seed file (2 tenants)"}
{"level":"info","msg":"listening","url":"http://127.0.0.1:3070", ...}MIOT_DASHBOARD_SEED=example loads sample data: two organizations, acme and globex, with a few people in each. MIOT_DASHBOARD_INSECURE_AUTH=true lets you say who you are with an x-dev-user header.
MIOT_DASHBOARD_INSECURE_AUTH trusts the header without checking it. The server only accepts it on localhost, and refuses to start with it in production. Use a real identity provider anywhere else. See Authenticate users.
List the dashboards
curl -H 'x-dev-user: alice' \
http://127.0.0.1:3070/tenants/acme/scopes/ops/dashboards{"data":[{"slug":"fleet","name":"Fleet overview"}]}alice is a Coordinator in scope ops of organization acme. Every address names the organization (tenants/acme) and the scope (scopes/ops).
Save a dashboard
curl -X PUT http://127.0.0.1:3070/tenants/acme/scopes/ops/dashboards/sales \
-H 'x-dev-user: alice' \
-H 'If-Match: "0"' \
-H 'Content-Type: application/json' \
-d '{
"version": 2,
"name": "Sales",
"preferences": { "editMode": false },
"widgets": [{
"id": "hello",
"componentId": "text_card",
"layout": { "i": "hello", "x": 0, "y": 0, "w": 12, "h": 3 },
"config": { "text": "My first widget" },
"createdAt": "2026-10-05T00:00:00Z",
"updatedAt": "2026-10-05T00:00:00Z"
}]
}'{"data":{"revision":1,"updatedAt":"2026-10-05T12:48:58.754Z"}}If-Match: "0" means “create it only if nobody has”. The answer carries the new revision, 1. Send it with the next save. Revisions and saving explains why.
See the server refuse
Save the same document again with the old revision:
curl -X PUT http://127.0.0.1:3070/tenants/acme/scopes/ops/dashboards/sales \
-H 'x-dev-user: alice' -H 'If-Match: "0"' \
-H 'Content-Type: application/json' -d '{"version":2,"name":"Sales","preferences":{"editMode":false},"widgets":[]}'{"error":"Dashboard was modified by someone else (expected revision 0, found 1)","status":409,"code":"CONFLICT"}Then try to read another organization’s dashboards as alice:
curl -H 'x-dev-user: alice' \
http://127.0.0.1:3070/tenants/globex/scopes/ops/dashboards{"error":"The requested scope is not accessible with these credentials","status":403,"code":"FORBIDDEN","reason":"TENANT_SCOPE"}alice does not belong to globex. The answer is the same as for an organization that does not exist, so nobody can use the address to find out which organizations exist.
Look at the API
Open http://127.0.0.1:3070/openapi.yaml for the full API definition. To browse it in Swagger UI at /docs, install swagger-ui-dist next to the server; the container image already includes it.
Next steps
- Draw this dashboard in a web page: Embed a dashboard in a web page.
- Build one in the ModularIoT web app: Build your first dashboard.
- Decide how you will run it: Choose a setup.
To start again from the sample data, stop the server and delete ./data.