MDK Logo

App Node plugins

Use the default App Node plugins, mount third-party plugins, and build your own using the mdk-plugin.json format

Overview

The Gateway exposes HTTP routes through a declarative plugin system. Each plugin is a directory containing an mdk-plugin.json manifest and one or more controller files. MDK ships a set of default plugins that load automatically; you can mount additional plugins for your own site logic.

Plugins call into the Kernel through services.mdkClient, an instance of @tetherto/mdk-client. No knowledge of the MDK Protocol envelope or internal message shapes is required.

Default plugins

MDK ships plugins that load automatically on Gateway startup:

  • The auth plugin serves identity and token endpoints under /auth
  • The telemetry plugin serves site metrics (hashrate, consumption, efficiency, temperature, and more) under /auth/metrics
  • The site-hashrate plugin serves aggregated site hashrate history

The plugin reference lists every default route, its method, and whether it needs a token — those tables are generated from each plugin's mdk-plugin.json. Plugins you mount yourself are documented by their own manifests.

Mount a plugin

Pass an extraPluginDirs array to startGateway() to load additional plugins at boot alongside the default plugins:

const { startGateway } = require('@tetherto/mdk')

await startGateway({
  kernel,
  port: 3000,
  extraPluginDirs: [
    path.join(__dirname, 'plugins/custom-metrics'),
    path.join(__dirname, 'plugins/alerts')
  ]
})

Each entry must be an absolute path to a directory containing an mdk-plugin.json. The plugin loader validates the manifest and all handler files at startup — missing files or invalid manifests throw immediately before the server comes up.

Build a plugin

A plugin is a directory with two things: a manifest and controllers.

1.1 Create the manifest

mdk-plugin.json declares the plugin identity (name, version) and a routes array. Each route needs an id, a handler path, and an http block with a method and path. Rather than copy a synthetic example, start from a real manifest and trim it:

Path parameters use {param} syntax — the loader normalises them to Fastify's :param format. For named exports use "handler": "./controllers/foo.js#namedExport". The plugin reference explains what each field means and what the loader requires.

1.2 Write a controller

Every controller exports an async function (req, services):

// controllers/live.js — read live telemetry
module.exports = async function live (req, services) {
  const deviceId = req.query.deviceId
  const telemetry = await services.mdkClient.pullTelemetry(deviceId, 'metrics')
  return { deviceId, ...telemetry }
}
// controllers/command.js — dispatch a command
module.exports = async function command (req, services) {
  const deviceId = req.params.deviceId
  const { mode } = req.body

  const result = await services.mdkClient.sendCommand(deviceId, 'setPowerMode', { mode })

  return {
    deviceId,
    commandId: result.commandId,
    status: result.status
  }
}

The req object

FieldTypeContains
req.paramsobjectPath parameters (e.g. { deviceId: 'wm-001' })
req.queryobjectQuery string parameters
req.bodyobjectParsed JSON request body
req.headersobjectHTTP headers
req._infoobjectInternal request metadata (rarely needed)

The services object

FieldTypeUse for
services.mdkClientMdkClientLive reads and command dispatch — sendCommand, pullTelemetry, getCapabilities, listWorkers
services.dataProxyDataProxyHistorical and aggregated data from Worker tail-logs — requestData, requestDataMap
services.authLibAuthLibJWT and session helpers (needed only for advanced auth flows)
services.confobjectGateway runtime config

Always guard services.mdkClient — it is null when the Gateway starts without a live Kernel connection:

if (!services.mdkClient) throw new Error('ERR_MDK_CLIENT_UNAVAILABLE')

Always guard services.authLib — it is undefined when the Gateway starts with noAuth: true. Any call to authLib.tokenHasPerms() or authLib.getTokenPerms() throws Cannot read properties of undefined in noAuth mode:

if (!services.authLib) throw new Error('ERR_AUTH_LIB_UNAVAILABLE')

Read hardware data

For live device data use mdkClient:

// Pull a live metrics snapshot
const tel = await services.mdkClient.pullTelemetry(deviceId, 'metrics')

// Pull the declared capabilities (from the Worker's mdk-contract.json)
const { capabilities } = await services.mdkClient.getCapabilities(deviceId)

// List all registered Workers
const { workers } = await services.mdkClient.listWorkers()

For historical or aggregated series from a Worker's persisted tail-log use dataProxy:

const results = await services.dataProxy.requestData('tailLogRangeAggr', {
  type: 'miner',
  startDate: start,
  endDate: end,
  fields: { hashrate_sum: 1 }
})

The default telemetry controllers show worked examples of both patterns.

Send a command

sendCommand dispatches via the Kernel to the Worker that owns the device. The command must be declared in the Worker's mdk-contract.json. It returns:

FieldTypeDescription
commandIdstringCorrelation ID generated by Kernel. Echo this to the HTTP caller so they can track the operation.
statusstring'SUCCESS' or 'FAILED'
resultobjectCommand-specific response payload (present when status is 'SUCCESS')
errorstringError message (present when status is 'FAILED')
const result = await services.mdkClient.sendCommand(deviceId, 'reboot', {})
if (result.status === 'FAILED') throw new Error(result.error)
return { commandId: result.commandId, status: result.status }

Auth, permissions, and caching

Auth — set "auth": true on a route to require a valid Bearer token. The adapter runs authCheck before the handler is called.

Permissions — add a "permissions" array to enforce RBAC:

{
  "id": "site.miners.command",
  "auth": true,
  "permissions": ["actions:w"],
  "handler": "./controllers/command.js",
  "http": { "method": "POST", "path": "/site/miners/{deviceId}/command" }
}

Caching — add a "cache" array of dot-path strings to enable request-level caching. The cache key is composed from the route ID and the resolved values of each path:

{
  "id": "telemetry.hashrate",
  "cache": ["query.start", "query.end", "query.groupBy"],
  ...
}

Pass ?overwriteCache=true to any cached route to bypass and refresh.

Manifest validation errors

The plugin loader validates every manifest and handler at startup and throws if anything is wrong:

ErrorCause
ERR_PLUGIN_MANIFEST_MISSINGNo mdk-plugin.json found in the plugin directory
ERR_PLUGIN_MANIFEST_INVALIDJSON parse error, or missing required field (name, version, or routes)
ERR_PLUGIN_ROUTE_DUPLICATE_IDTwo routes in the same manifest share the same id
ERR_PLUGIN_HANDLER_NOT_FOUNDThe handler file path does not exist or failed to load
ERR_PLUGIN_HANDLER_NOT_FUNCTIONThe handler file exports something other than a function

Troubleshooting

Migrate from v0.2 to v0.3

In v0.3, metricsRoutes and devicesRoutes were removed from backend/core/gateway/workers/lib/server/index.js. The auth and telemetry endpoints they registered are now delivered by the default plugins, which load automatically — no action needed for those.

If your v0.2 code patched or monkey-patched those registrations to inject custom logic:

  1. Create a plugin directory with an mdk-plugin.json and controller files for the routes you were injecting.
  2. Pass the directory to startGateway() via extraPluginDirs.
// Before (v0.2 — no longer works)
const server = require('@tetherto/mdk-gateway/workers/lib/server')
server.metricsRoutes.push(myCustomRoute)

// After (v0.3+)
await startGateway({
  kernel,
  extraPluginDirs: [path.join(__dirname, 'plugins/my-metrics')]
})

Next steps

  • Try the live site backend example for a complete worked plugin with three routes: a live site overview, a historical series, and a command endpoint running under PM2 or Docker
  • Build the minimal dashboard tutorial — end-to-end worked example of the single-plugin + controller pattern
  • Understand how Workers declare their data via mdk-contract.json — what mdkClient reads and sendCommand dispatches
  • See the full manifest and services reference
  • Review the Gateway API and config

On this page