> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myrax.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime

> Backend processes: environment, transports, lifecycle

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:

```toml theme={null}
[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:

| Variable                 | Meaning                                                 |
| ------------------------ | ------------------------------------------------------- |
| `MYRAX_PLUGIN_ID`        | your plugin id                                          |
| `MYRAX_PLUGIN_DIR`       | plugin install directory (also the working directory)   |
| `MYRAX_PLUGIN_DATA`      | writable per-plugin data directory — persist state here |
| `MYRAX_PLUGIN_PORT`      | TCP port to bind on `127.0.0.1` (tcp transport)         |
| `MYRAX_PLUGIN_TRANSPORT` | `tcp` or `unix`                                         |
| `MYRAX_PLUGIN_SOCKET`    | unix socket path to listen on (unix transport)          |
| `MYRAX_CORE_URL`         | local URL of the panel itself                           |
| `MYRAX_PLUGIN_LOG`       | path of your log file                                   |

## A complete runtime

Bind the address you're given, answer `/health`, store state in the data dir:

<CodeGroup>
  ```js runtime/server.js theme={null}
  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)));
  ```

  ```go runtime/main.go theme={null}
  package main

  import (
  	"context"
  	"encoding/json"
  	"log"
  	"net/http"
  	"os"
  	"os/signal"
  	"path/filepath"
  )

  func main() {
  	port := os.Getenv("MYRAX_PLUGIN_PORT")
  	dataDir := os.Getenv("MYRAX_PLUGIN_DATA")
  	statePath := filepath.Join(dataDir, "state.json")

  	mux := http.NewServeMux()
  	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
  		w.Header().Set("Content-Type", "application/json")
  		w.Write([]byte(`{"ok":true}`))
  	})
  	mux.HandleFunc("GET /api/state", func(w http.ResponseWriter, r *http.Request) {
  		state := map[string]any{}
  		if raw, err := os.ReadFile(statePath); err == nil {
  			json.Unmarshal(raw, &state)
  		}
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(state)
  	})

  	server := &http.Server{Addr: "127.0.0.1:" + port, Handler: mux}

  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
  	defer stop()
  	go func() {
  		<-ctx.Done()
  		server.Shutdown(context.Background())
  	}()

  	log.Printf("listening on %s", port) // goes to the plugin log
  	server.ListenAndServe()
  }
  ```
</CodeGroup>

For a Go runtime, compile it for the server and reference the binary:

```toml theme={null}
[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 route                      | Hits 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.
