This page is a complete Node.js manual for implementing MachineID runtime enforcement without relying on any specific agent framework.
- Express / Fastify / HTTP services
- Workers that consume queues
- Cron jobs and scheduled tasks
- Serverless functions (Lambda, Vercel, etc.)
- Any runtime that can incur cost or side effects
- A “soft guardrail” tutorial
- A feature-flag system
- A best-effort policy guide
Everything reduces to one invariant:
Register. Validate. Work.
If validation fails, work does not begin.
Generate a free org key (supports up to 3 devices): machineid.io
In Node, boundary placement determines stop latency: validate at the points where work begins, and immediately before cost and side effects.
- Path A (recommended): Use a local
guard.jshelper (provided below) - Path B: Direct HTTP to canonical endpoints (POST register + validate)
In Node.js, if you call validate and forget await, your runtime can execute work before the authority decision completes.
That is a fail-open bug.
MachineID enforces permission on an identity (a “device”) that represents an execution surface. In Node, an execution surface is usually one of:
- One service instance (one container / one process)
- One queue worker process (one consumer replica)
- One scheduled job identity
- One tool runner / side-effect executor
- One serverless function invocation surface
- Good: one worker replica = one device
- Good: one serverless function identity per function name + env
- Risky: one entire fleet = one device (loses surgical control)
Device IDs should be stable enough to audit, but specific enough to revoke. A practical format:
{service}:{env}:{role}:{instance}
Examples:
api:prod:http:01worker:prod:queue-consumer:07job:prod:nightly-reindex:01tool:prod:payments-runner:02fn:prod:invoice-webhook:01
MachineID uses canonical POST endpoints. Send your org key via x-org-key.
POST https://machineid.io/api/v1/devices/register
Headers:
x-org-key: org_...
Content-Type: application/json
Body:
{"deviceId":"worker:prod:queue-consumer:07"}
POST https://machineid.io/api/v1/devices/validate
Headers:
x-org-key: org_...
Content-Type: application/json
Body:
{"deviceId":"worker:prod:queue-consumer:07"}
Validation returns a decision including allowed, a stable code, and a request_id.
Your runtime must treat allowed:false as an immediate stop condition.
- If
allowedis false: do not begin work - Log
code+request_idfor auditability - Stop the loop / stop the handler / terminate the worker
Validation belongs at execution boundaries: points where work begins or commits side effects. Most production Node systems already have these boundaries; they are simply not treated as enforcement points.
- Startup boundary: before an instance begins processing work
- Request/job boundary: before each request or unit of work
- Tool boundary: before external calls that incur cost
- Side-effect boundary: before irreversible actions (writes, payments, emails)
- Fan-out boundary: before batch loops and parallel dispatch
Pattern A: Startup gate (required)
Register once, validate once, then start serving/consuming work. If validation denies, terminate the process.
Pattern B: Per-request / per-job gate (recommended)
Validate before each request/job. This makes revocation effective at predictable points.
Pattern C: Tool-call gate (high leverage)
Validate immediately before each external call that incurs spend or cost.
Pattern D: Side-effect gate (most leverage)
Validate immediately before irreversible actions: writes, deletes, payments, emails, queue publishes.
In serverless environments, “startup” is ambiguous (cold start vs warm start). For strict enforcement: treat every invocation as a startup boundary.
- Validate at the top of the handler, every time
- Validate again immediately before high-cost tools and side effects
- Fail closed on timeout/network errors
MachineID does not introspect internal loops. If a Node process runs for hours, you must define enforcement boundaries inside the loop.
- Before each iteration that triggers a tool call
- Before each external request that can incur cost
- Before each write or irreversible side effect
- Before entering high-cost fan-out loops
Side effects are where runtime mistakes become expensive or irreversible: payments, emails, writes, deletes, credential rotations, queue publishes.
- Treat side-effect functions as privileged surfaces
- Validate immediately before the side effect
- Fail closed: if you cannot confirm permission, do not execute
Node developers commonly place enforcement behind a small guard. The guard must:
(1) best-effort register, (2) hard-gate validate, (3) fail closed on any uncertainty, and
(4) require await.
// guard.js
const process = require("node:process");
const MACHINEID_BASE_URL = "https://machineid.io";
const REGISTER_URL = `${MACHINEID_BASE_URL}/api/v1/devices/register`;
const VALIDATE_URL = `${MACHINEID_BASE_URL}/api/v1/devices/validate`;
function die(msg) {
// minimal ops visibility
try { process.stderr.write(msg + "\n"); } catch (_) {}
process.exit(1);
}
async function postJson(url, orgKey, body, timeoutMs) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-org-key": orgKey,
},
body: JSON.stringify(body),
signal: ctrl.signal,
});
return resp;
} finally {
clearTimeout(t);
}
}
async function enforce(deviceId, orgKey, { timeoutMs = 2000 } = {}) {
if (!orgKey) die("authorization guard: missing org key");
if (!deviceId) die("authorization guard: missing device id");
// 1) REGISTER (best effort; does not authorize)
try {
await postJson(REGISTER_URL, orgKey, { deviceId }, timeoutMs);
} catch (_) {
// visibility but still continue to validate (validate is authoritative)
try { process.stderr.write("authorization guard: registration failed (non-fatal; validate will gate)\n"); } catch (_) {}
}
// 2) VALIDATE (hard gate; MUST be awaited by the caller)
let resp;
try {
resp = await postJson(VALIDATE_URL, orgKey, { deviceId }, timeoutMs);
} catch (_) {
die("authorization guard: validate unreachable");
}
if (!resp || !resp.ok) {
die(`authorization guard: validate failed (http ${resp ? resp.status : "unknown"})`);
}
let data;
try {
data = await resp.json();
} catch (_) {
die("authorization guard: validate failed (non-json response)");
}
if (!data || data.allowed !== true) {
const code = (data && data.code) ? data.code : "UNKNOWN";
const requestId = (data && data.request_id) ? data.request_id : "UNKNOWN";
die(`authorization guard: denied (code=${code}, request_id=${requestId})`);
}
return data;
}
module.exports = { enforce };
await enforce(...) before work begins. Do not fire-and-forget.
If you prefer to avoid a helper, call the endpoints directly. The rule stays the same: validate must be awaited and must complete before work.
await fetch("https://machineid.io/api/v1/devices/register", {
method: "POST",
headers: { "Content-Type": "application/json", "x-org-key": "org_..." },
body: JSON.stringify({ deviceId: "api:prod:http:01" })
});
const resp = await fetch("https://machineid.io/api/v1/devices/validate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-org-key": "org_..." },
body: JSON.stringify({ deviceId: "api:prod:http:01" })
});
const decision = await resp.json();
if (!decision.allowed) {
process.exit(1);
}
The free tier is enough to implement a complete control loop and prove stop behavior end-to-end. A practical three-device model:
api:dev:http:01— web/API runnerworker:dev:queue-consumer:01— consumes and processes jobstool:dev:external-call-runner:01— tool-heavy or cost-heavy actions
This tier supports multiple concurrent workers and multiple execution surfaces for tools and side effects.
- 8 HTTP replicas:
api:prod:http:01…:08 - 10 queue consumers:
worker:prod:queue-consumer:01…:10 - 4 tool runners:
tool:prod:external-call-runner:01…:04 - 3 schedulers:
job:prod:nightly:01…:03
At this scale, execution surfaces multiply under load: autoscaling, retries, event storms, and distributed consumers. The dominant requirement is consistent enforcement across replicas and across time.
- Autoscaling worker pools (many replicas)
- Multiple queues or topic partitions
- Per-tenant or per-workflow identities
- Dedicated side-effect runners
Near the upper end of standard caps, the dominant failure mode is execution multiplication: autoscale, fan-out, retries, recursion, and delayed resumes.
- Prefer per-replica identities (avoid a single “fleet” identity)
- Validate at boundaries that occur during real work (tool + side-effect)
- Avoid fallback authority paths (no degraded enforcement)
If you need device limits beyond standard tiers, MachineID supports custom device limits. This does not require changes to runtime code — the identity model and enforcement boundaries remain the same.
The console lives outside your runtime: machineid.io/dashboard
- Revoke / restore devices (including bulk)
- Remove devices
- Register devices
- Rotate key
- Lost key recovery via magic link
- Org-wide disable
In addition to revoking individual devices, MachineID supports an org-wide disable control. This affects validate outcomes across the org.
- Org disable does not change device revoked/restored states
- It causes validate to deny across the org
- It takes effect at the next validation boundary you defined
Revoke/restore and org disable are effective at the next validate. Stop latency is determined by boundary placement.
- Validate before tool calls
- Validate before side effects
- Validate at loop re-entry points
- Validate before resuming long-paused work
Replace bracketed placeholders and paste into your LLM of choice.
I have a Node.js worker/service runtime and I want hard enforcement using MachineID.io.
Context:
- Org key: [PASTE ORG KEY]
- Device ID scheme: {service}:{env}:{role}:{instance}
- Fail-closed policy, short timeout (1–3s)
- Critical Node requirement: validate MUST be awaited (no fire-and-forget promises)
Validation boundaries required:
1) Startup (register + validate)
2) Before each unit of work (request/job/message)
3) Before any external tool call that can incur cost
4) Before any irreversible side effect (writes, payments, emails)
5) Before entering high-cost fan-out loops or batches
Please provide:
1) Exact files/functions where validation should be added
2) Copy/paste code blocks (guard.js pattern + direct fetch pattern)
3) A recommended boundary map for my runtime
4) A test plan using the MachineID.io dashboard:
- revoke a device
- restore it
- use org-wide disable
- verify work stops at the next validate boundary
- Stable device identity per execution surface
- Startup gating (register + validate, fail closed)
- At least one stop point during real work (tool-call or side-effect boundary)
- Short timeout and consistent failure policy
- Denials logged as operational events (include request_id)
- Node-specific requirement: await validation
- Dashboard: machineid.io/dashboard
- Core guidance: External Control Plane, Implementation Guide, Operational Guarantees
- MachineID GitHub org: github.com/machineid-io