Skip to main content
A runtime is any executable the panel starts alongside your plugin — Node, Go, Python, a shell script, anything. The panel supervises it, captures its output, and proxies HTTP and websocket traffic to it under the panel’s own auth. Declared in the manifest:
[runtime]
enabled = true
command = "/usr/bin/env"
args = ["node", "runtime/server.js"]
transport = "tcp"   # or "unix"
port = 0            # 0 = panel picks a free port
A relative command resolves against the plugin directory — you can ship a compiled binary inside the plugin and point at it directly.

Environment

The panel passes everything your process needs as environment variables:
VariableMeaning
MYRAX_PLUGIN_IDyour plugin id
MYRAX_PLUGIN_DIRplugin install directory (also the working directory)
MYRAX_PLUGIN_DATAwritable per-plugin data directory — persist state here
MYRAX_PLUGIN_PORTTCP port to bind on 127.0.0.1 (tcp transport)
MYRAX_PLUGIN_TRANSPORTtcp or unix
MYRAX_PLUGIN_SOCKETunix socket path to listen on (unix transport)
MYRAX_CORE_URLlocal URL of the panel itself
MYRAX_PLUGIN_LOGpath of your log file

A complete runtime

Bind the address you’re given, answer /health, store state in the data dir:
const http = require('http');
const fs = require('fs');
const path = require('path');

const port = Number(process.env.MYRAX_PLUGIN_PORT);
const dataDir = process.env.MYRAX_PLUGIN_DATA;
const statePath = path.join(dataDir, 'state.json');

function readState() {
  try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); }
  catch { return {}; }
}

function writeState(state) {
  fs.mkdirSync(dataDir, { recursive: true });
  fs.writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
}

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end('{"ok":true}');
  }
  if (req.method === 'GET' && req.url === '/api/state') {
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end(JSON.stringify(readState()));
  }
  res.writeHead(404, { 'content-type': 'application/json' });
  res.end('{"error":"not found"}');
});

server.listen(port, '127.0.0.1', () => {
  console.log(`listening on ${port}`); // goes to the plugin log
});

process.on('SIGINT', () => server.close(() => process.exit(0)));
For a Go runtime, compile it for the server and reference the binary:
[runtime]
enabled = true
command = "runtime/my-plugin"   # relative to the plugin directory
transport = "tcp"
port = 0

Reaching it from the browser

The runtime listens on localhost only; the panel exposes it through two authenticated routes:
Panel routeHits your runtime at
/api/plugins/{id}/proxy/{path}http://127.0.0.1:{port}/{path} (any method)
/api/plugins/{id}/ws/{path}websocket upgrade to /{path}
So fetch('/api/plugins/hello/proxy/api/state') from your frontend lands on GET /api/state in your runtime — with the panel’s session auth already enforced.

Lifecycle

  • The runtime starts with the panel (if the plugin is enabled) and on install / enable / myrax plugin restart <id>.
  • On stop the panel sends SIGINT, waits 3 seconds, then kills. Handle SIGINT for a clean shutdown.
  • If the process exits on its own, its status shows failed with the exit error in the Plugins screen.
  • stdout and stderr go to a per-plugin log: myrax plugin logs <id>.

Unix sockets

For transport = "unix" the panel skips ports entirely: listen on the path from MYRAX_PLUGIN_SOCKET (it lives inside your data directory) and the proxy routes work exactly the same.