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

# Node runtime

> The entry module exports start(context). The host launches the process, assigns the port, guards access, and cleans up.

## Entry contract

* The entry is an ES module that exports `start(context)`. Return from `start` only after the HTTP listener accepts connections and every declared route is installed. That is the readiness signal; you don't write a health-check route.
* In `start`, only register routes and start listening. Don't fetch business data synchronously; load it on demand in request handlers.
* Return `{ dispose }`. `dispose` closes the listener, SSE / WebSocket connections, child processes, timers, subscriptions, and persistence handles. It is not a per-request completion callback.
* The entry process is the one the host manages directly. It must stay alive, holding its assigned port, for as long as the Mini App is available.
* No Mini App SDK is installed at runtime. The `miniapp-api.ts` file from the Skill is for type checking only.

## context fields

| Field | Description |
| - | - |
| `pluginId` / `pluginRoot` | The adopted plugin identity and the directory of its runtime copy |
| `dataDir` | A data directory the host isolates for this plugin. Keep runtime state here, not in the workspace or the payload |
| `listen` | The assigned loopback address `{ host: '127.0.0.1', port }`. Don't choose or probe a port yourself |
| `signal` | An `AbortSignal` that cancels startup, requests, and background work |
| `logger` | Structured logging; pass stable event codes and non-sensitive fields |
| `hostConnector?` | Present only when the manifest declares it and the host authorizes it; see [Data access](/docs/miniapp/authors/data#host-connector) |
| `agent?` | Present only when `agentAccess` is declared; see [Agent API](/docs/miniapp/authors/agent#agent-api) |

## Minimal entry

```javascript miniapp/node/server.mjs theme={null}
// @ts-check
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';

/** @typedef {import('./miniapp-api.js').MiniAppContext} MiniAppContext */
/** @typedef {import('./miniapp-api.js').MiniAppLifecycle} MiniAppLifecycle */

/**
 * @param {MiniAppContext} context
 * @returns {Promise<MiniAppLifecycle>}
 */
export async function start(context) {
  const clientEntry = await readFile(join(context.pluginRoot, 'miniapp/client/index.html'));
  const server = createServer((request, response) => {
    const url = new URL(request.url ?? '/', 'http://miniapp.local');
    if (request.method === 'GET' && url.pathname === '/dashboard') {
      response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
      response.end(clientEntry);
      return;
    }
    response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
    response.end(JSON.stringify({ error: 'not_found' }));
  });

  await listen(server, context.listen.host, context.listen.port);
  context.logger.info('miniapp.runtime.listening');

  let disposed = false;
  const dispose = async () => {
    if (disposed) return;
    disposed = true;
    context.signal.removeEventListener('abort', onAbort);
    await close(server);
  };
  const onAbort = () => void dispose();
  context.signal.addEventListener('abort', onAbort, { once: true });
  if (context.signal.aborted) await dispose();

  return { dispose };
}

function listen(server, host, port) {
  return new Promise((resolve, reject) => {
    const onError = (error) => reject(error);
    server.once('error', onError);
    server.listen(port, host, () => {
      server.off('error', onError);
      resolve();
    });
  });
}

function close(server) {
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}
```

## When it runs

* **On demand:** it starts when a Mini App is selected with `@`, its page is opened, or on `publish` or `restart`. Enabling the plugin doesn't start it.
* **Shared service:** each Mini App has one Node process shared by all conversations, and each conversation gets its own page. See [Mini Apps and sessions](/docs/miniapp/sessions).
* **Capacity:** at most 3 different Mini App services run at once. A fourth returns `BUSY`; updating a running Mini App doesn't take an extra slot.
* **Stop:** `stop` only stops the service and keeps the plugin and its other capabilities. Closing a running Mini App's tab and confirming stops its shared service and closes that plugin's pages in every conversation.
* **Update and restart:** the old process stops before the new one starts. A Mini App never has more than one process, so don't start a second entry yourself.

## Other languages and child processes

Anything can sit behind the entry. For a Python, Go, or other backend, have the Node entry launch it on its own internal port and reverse-proxy to it from the assigned port.

> [!WARNING]
>
> The host manages only the entry Node process. Close any child processes the plugin starts in `dispose`; the host doesn't promise to find and end them.

## Port authentication

Before loading the entry, the host installs an authentication guard on the `node:http` servers in the process. Only the Mini App page and the managed MCP client carry the current run's credential; every other request gets `401` before it reaches plugin code. Plugins need no auth logic and never see the credential. See [Ports and credentials](/docs/miniapp/host/security#guard) for how it works.

* Serve the assigned port with `node:http`, either `createServer` or a framework built on it. If `https`, `http2`, a raw `net` server, or a child process binds the port directly, the guard can't protect it, the service never becomes ready, and startup fails with `READINESS_TIMEOUT`.
* The `/__mavis/` path prefix belongs to the host. Don't declare or handle routes under it.
* Build page requests from relative or same-origin URLs. Service Workers, pop-ups, external browsers, and ordinary browser tabs don't carry the credential and get `401`.
* The guard covers every `node:http` server in the process. To receive third-party callbacks such as OAuth redirects, route them through the assigned port instead of opening another HTTP server in the process.
* A restart issues a new credential. An already open tab gets `401` until the user refreshes or reopens it; this is expected.

## Logging

Use `context.logger` for diagnostics. The host collects only bounded stdout / stderr samples, so keep credentials and business content out of logs and don't rely on them being retained in full.
