---
title: "Enrol and watch your devices"
description: "Declare device, make an enrolment token, enrol a device with its own key, read its health, receive its telemetry once and in order, and revoke it."
section: "guides"
status: "Building for 1.3"
spec: "device-identity"
canonical: "https://mainsdk.dev/docs/guides/enrol-and-watch-your-devices"
updated: "2026-10-28"
---

# Enrol and watch your devices

> Declare device, make an enrolment token, enrol a device with its own key, read its health, receive its telemetry once and in order, and revoke it.

Status: Building for 1.3 · Canonical: https://mainsdk.dev/docs/guides/enrol-and-watch-your-devices · Updated: 2026-10-28

You will let a program on a machine enrol with your app, prove who it is with a key it made, report its health and send records that a handler of yours receives once and in order. Then you will revoke it. It takes about 20 minutes. Devices call your app; nothing here opens a port on a device.

## You need

- A MainSDK app with a `main.yaml` and a signed-in operator who holds `device.manage` and `device.view`.
- A secret for the authority key that signs credentials: a PKCS#8 Ed25519 key as base64, stored as `MAINSDK_DEVICE_KEY`. In development the SDK makes a key for you; `mainsdk check` reports `MAIN-DEVICE-0016` if production has none.

## 1. Declare the section and import the module

```yaml title="main.yaml"
device:
  authority: secret:MAINSDK_DEVICE_KEY
  enrol: { proofs: [token] }
  attest: { require: software }
  heartbeat: { every: 60s, late-after: 2, silent-after: 5 }
  telemetry:
    streams:
      app.status: { redact: [user.email] }
```

```ts title="app/server.ts"
import { device } from "mainsdk/device";   // not exported from "mainsdk"
```

With no `device` section nothing is mounted, stored or timed, even when you import the module. Declaring the section without the import is `MAIN-DEVICE-0002`; importing without the section is `MAIN-DEVICE-0001`. The adapters serve `device.handler()` at `device.path` for you.

## 2. Make an enrolment token

```ts
import { device } from "mainsdk/device";

const { token } = await device.enrolments.create({ group: "lab", uses: 10 });
```

The token is shown once and only its hash is stored. One token takes at most ten attempts a minute; past that a device gets `MAIN-DEVICE-0017`.

## 3. Enrol the device

```ts title="on the device"
import { deviceClient, fileKeyStore, softwareEvidence } from "mainsdk/device/client";

const client = deviceClient({
  url: "https://app.example.com/mainsdk/device",
  keys: fileKeyStore("/var/lib/acme"),
  attest: softwareEvidence({ software: { name: "acme-agent", version: "2.1.0" } }),
});
await client.enrol({ token: "mde_paste-the-token-here" });
await client.start();
client.send("app.status", { ok: true });
```

The key is made on the device and never leaves it; the key file has mode 0600 in a folder with mode 0700, and a readable key file is refused with `MAIN-DEVICE-0015`. The device joins the token's tenant as `pending` and becomes `online` at its first heartbeat.

## 4. Receive telemetry

```ts
import { device } from "mainsdk/device";

device.telemetry.on("app.status", async (records) => {
  console.log(`received ${records.length} records`);
});
```

A batch is acknowledged only after every handler resolves. A handler that throws makes the device resend the same batch with the same record ids, so write your handler to be safe to repeat. Records with a secret or personal data the stream did not allow are refused with `MAIN-DEVICE-0034`; the rest go on.

## 5. Read health

```ts
import { device } from "mainsdk/device";

const lab = await device.list({ group: "lab", state: "silent" });
const counts = await device.summary.get();
```

A device is `late` after two missed heartbeats and `silent` after five; each silence raises one `device.silent` signal. Time your own app was down is left out, so an outage of yours never turns a fleet silent.

## 6. Revoke

```ts
import { device } from "mainsdk/device";

await device.revoke("dvc_01JC8Q4Z7N0ABCDEFGHJKMNPQR", { reason: "lost" });
```

Every instance refuses the device from its next request with `MAIN-DEVICE-0008` and a signed notice, and the client deletes its key. A revoked id is never reactivated; the device enrols again under a new id.

## 7. Test it

```ts
import { createTestApp, simulateDevices } from "@mainsdk/testing";

const app = await createTestApp({ yaml: "schema: 1\ndevice:\n  driver: local\n" });
const sim = await simulateDevices(app, { count: 50 });
sim.offline(5);
await sim.run("10m");
```

`simulateDevices` drives real clients on the fake clock, so a test of silence takes milliseconds. `sim.tamper(id, "replay")` breaks one device's next request and the refusal appears in `sim.refused`.

## Check it

```sh
pnpm mainsdk check
```

The check passes when the section is valid, the module is imported, every telemetry handler names a declared stream and production has an authority key.
