See an app serve your data
[⏱️ ~5 min] Scaffold a Worker and an app with the CLI, run the stack, and watch the app answer an HTTP request with live fleet data
If Kernel, Gateway, Worker, or app are unfamiliar, read terminology first.
Overview
The Gateway serves no data routes of its own. It routes HTTP requests to apps, so every endpoint your application reads comes
from an app your stack declares. This tutorial scaffolds one with the mdk CLI and runs it so you can watch that happen once
before writing your own.
What you'll have at the end:
- A mock miner Worker with one seed device, scaffolded by
mdk create worker - An app with one read-only route, scaffolded by
mdk create appand running in its own process - A Gateway on
:3847forwardingGET /apps/fleet/api/fleet/summaryto that app - The JSON body of that request printed to your terminal, counting the Workers and devices the Kernel knows about
Prerequisites
- Node.js >=24 (LTS)
- npm 11 (< 12)
- The
mdkCLI installed - Outbound UDP to the HyperDHT bootstrap nodes
jq, to read the access token out of the sign-in response
Create a project
mkdir fleet-demo && cd fleet-demoWrite a minimal mdk.yaml:
apiVersion: mdk/v1
kind: Stack
metadata:
name: fleet-demo
spec:
workers: []
gateway:
port: 3847
apps: []mdk onboard writes the same file interactively; this tutorial keeps it to the fields it uses.
Scaffold a Worker and an app
mdk create worker demo-miner
mdk create app fleetEach command copies a template, links the package into the project's package.json, runs npm install, and adds an entry to
mdk.yaml: the Worker under spec.workers with a mock: true seed device, the app under spec.apps. mdk create app also
builds the app's starter UI into ui/ and writes the app's grant into spec.rbac: the role and binding that give it the
Kernel access its manifest requests.
Run the stack
mdk rbac init
mdk runmdk rbac init completes spec.rbac: it binds the CLI's own account to an admin role and reports the app grant that
mdk create app already wrote as in place. Without the admin binding mdk run refuses to boot a Kernel nobody could
administer.
mdk run boots the Kernel, the Worker and its mock device, one runtime process for the app, then the Gateway. Leave it running.
No app route is open, so create the first operator from a second terminal, sign in, and call the route with
the access token:
mdk user add ops@example.com --admin # prompts for a password of at least 12 characters; the Kernel must still be running
TOKEN=$(curl -s -X POST http://localhost:3847/auth/login \
-H 'content-type: application/json' \
-d '{"username":"ops@example.com","password":"<the password you set>"}' | jq -er .accessToken)
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3847/apps/fleet/api/fleet/summaryExpected output:
{"ok":true,"kernelConnected":true,"workerCount":1,"deviceCount":1,"workers":[{"workerId":"demo-miner","devices":1}]}If mdk run reports every component ready but mdk user add answers Kernel unreachable, the machine has no outbound UDP
to the DHT bootstrap nodes: the components booted, but the CLI cannot reach the Kernel from a second terminal. Open the
network as the prerequisites describe, or run the stack on the local and air-gapped profile.
A workerCount of 0 means the Worker has not registered yet; call again after a few seconds. That JSON came from a route the
Gateway did not have until the stack declared an app that serves it.
Read the files that produced it
The parts of the app this tutorial exercises are a manifest, a route table and a controller. The scaffold also ships a starter UI, which no step here opens:
apps/fleet/
├── mdk-app.yaml # id, version, displayName, permissions, config defaults
├── plugin/
│ ├── mdk-plugin.json # the route table the Gateway reads
│ ├── lib/client.js # the app's MDK client
│ └── controllers/summary.js # the handler
├── ui-src/ # starter page source, not used in this tutorial
└── ui/ # that page, built by `mdk create app`plugin/mdk-plugin.json declares the route: an id, the handler path, an http block giving the method and path, and
"safety": "read-only", which is what lets mdk run mcp offer the route to an agent as a tool.
plugin/controllers/summary.js is the handler, shown abridged. Every controller is async function (req) and returns a
plain object, which is sent back as JSON. It never touches a response object:
const mdkClient = require('../lib/client')
module.exports = async function summary (req) {
const resp = await mdkClient.listWorkers()
const workers = (resp && resp.workers) || []
// ... count devices across every Worker and return the counts
}The generated file also catches ERR_MDK_CLIENT_UNAVAILABLE and answers kernelConnected: false, so the route stays
callable while the Kernel is still booting.
mdkClient, built once for the app in plugin/lib/client.js from require('@tetherto/mdk-gateway/plugin'), is the same
protocol client used everywhere else in MDK. Aggregation lives here, in the app. Workers are structurally single-device,
so a Worker can only ever answer for one device; combining them into a fleet view is the app's job.
The route answered because --admin bound ops@example.com to a role holding wildcards. Every app route is checked against spec.rbac:
a request with no token answers 401, and a signed-in caller with no rule over fleet/api/fleet/summary answers 403.
Narrowing a user down to the resources they should reach is covered in
auth and permissions.
What just happened
- Scaffolding copied a Worker template (a mock miner: a small HTTP JSON device API) and an app template into
workers/demo-minerandapps/fleet, and registered both inmdk.yaml. - Kernel started fail-closed. Each caller, the app runtime included, published its key to
.mdk/trusted-keys/, which is how the Kernel admits it. Admission only lets a caller connect: every request past sign-in carries an access token the Kernel checks againstspec.rbac. - Worker started with its mock device and registered with the Kernel in the same process. Run on its own with
mdk run worker, it publishes its key to.mdk/keys/instead, where the Kernel discovers it. - App runtime started
apps/fleetin its own process, loaded its route table and controller, and accepted calls only from the Gateway's key. - Gateway signed
ops@example.comin at/auth/login, checked the access token and the rule overfleet/api/fleet/summary, then read the app'smdk-plugin.json, mounted its route under/apps/fleet, and forwarded your request to the app runtime over HRPC. - The controller called
listWorkers()on the Kernel and returned the counts as JSON.
Cleanup
Stop mdk run with Ctrl+C, then remove the project:
cd .. && rm -rf fleet-demoNext steps
- Write your own routes by building an app: the manifests, controllers, reading hardware, and sending commands
- Put a page in front of your route by building a minimal single-page dashboard: the same shape with a React front end
- Integrate your own hardware by building a third-party Worker
- See the whole stack instead of one route by running a mining site end to end