Skip to content

Runtime

Modular runtime for Bun with a worker pool, plugin system, and micro-frontend support. The main process orchestrates requests but never executes application code — that work is isolated in workers (see Worker Pool).

LayerTechnology
RuntimeBun (Bun.serve, Worker, Bun.file)
HTTP frameworkHono
ValidationZod
LRU cachequick-lru
Versioningsemver
API docshono-openapi, @scalar/hono-api-reference
apps/runtime/src/
├── index.ts # Entry: Bun.serve + graceful shutdown
├── api.ts # Initializes logger, config, pool, plugins, routes
├── app.ts # Hono app: CSRF, hooks, request resolution
├── config.ts # Loads RUNTIME_* env vars
├── constants.ts # Zod validation of PORT/NODE_ENV, BodySizeLimits
├── libs/pool/ # WorkerPool, WorkerInstance, wrapper
├── plugins/ # PluginLoader, PluginRegistry
├── routes/ # apps, health, plugins, admin, worker
└── utils/ # request, serve-static, get-entrypoint, get-worker-dir

Initialization happens in layers, each depending on the previous one:

StepModuleResponsibility
1constants.tsValidates PORT, NODE_ENV, DELAY_MS; defines IS_DEV, IS_COMPILED
2config.tsResolves RUNTIME_WORKER_DIRS (required), RUNTIME_PLUGIN_DIRS, RUNTIME_POOL_SIZE
3loader.tsScans pluginDirs, reads manifest.yaml, filters enabled, sorts by dependencies
4api.tsCreates logger, WorkerPool, PluginRegistry, mounts core routes and Hono app
5index.tsStarts Bun.serve, runs runOnServerStart, registers SIGINT handler
AspectDevelopmentProduction
poolSize10500
Loggerpretty (colored)json (structured)
Log leveldebuginfo
HMREnabledDisabled

Other defaults: staging = 50 workers, test = 5.

Bun.serve is configured in index.ts with a few operational quirks:

OptionValueReason
idleTimeout0Disables timeout so SSE/WebSocket connections stay open
routes["/favicon.ico"]204 No ContentPrevents 404s in logs
routespluginRoutesserver.routes aggregated from plugins
development.hmrtrue (dev)Hot Module Replacement
websocketcombinedSingle handler aggregating all plugins

SIGINT triggers a pipeline with a total timeout of 30s (SHUTDOWN_TIMEOUT_MS):

  1. Arms a force-exit timer (process.exit(1) in 30s).
  2. registry.runOnShutdown() — plugin hooks in reverse order (LIFO).
  3. pool.shutdown() — terminates all workers.
  4. logger.flush().
  5. clearTimeout + process.exit(0).

Any failure in the chain falls to the catch block and forces exit code 1.

Request -> CSRF (/api/*) -> onRequest hooks -> server.fetch -> plugin.routes
-> plugin app (worker) -> worker app -> onResponse hooks -> Response

Applied to /api/* for state-mutating methods (POST, PUT, PATCH, DELETE):

ConditionBehavior
Method in [GET, HEAD, OPTIONS]Bypass
Header X-Buntime-Internal: trueBypass (worker → runtime)
Sec-Fetch-Mode present without Origin403
Origin.host !== request.host403

Constants in constants.ts: DEFAULT = 10MB, MAX = 100MB. Configurable via env (BODY_SIZE_DEFAULT, BODY_SIZE_MAX) and per worker in manifest.yaml (maxBodySize: 50mb). If maxBodySize > MAX, the runtime emits a warning and uses MAX.

Validation happens in two steps:

  1. Fast path: invalid Content-Length or larger than limit → 413 Payload Too Large.
  2. Slow path (chunked): full read, recheck of actual size.

Everything returns BodyTooLargeError in application code. The response includes the X-Request-Id header for log correlation.

rewriteUrl(url, basePath) removes the path prefix while preserving the query string — used before injecting into the worker. The function assumes the path starts with basePath (validated upstream).

InputResult
basePath = ""Returns original pathname
pathname === basePathReturns "/"
pathname does not start with basePathUndefined behavior — validate upstream
HeaderDirectionDescription
X-Baseruntime → workerBase path injected for SPAs
X-Buntime-Internalworker → runtimeBypasses CSRF
X-Not-Foundruntime → shellSignals consistent 404 rendering
X-Request-IdbidirectionalCorrelation UUID

Resolution in app.ts follows a strict priority order. More specific routes (plugins) take precedence over generic ones (workers):

OrderLayerExample
1CSRFBlock before everything else
2App-shell modeshouldRouteToShell() intercepts navigation
3onRequest hooksAuth, rate limiting, metrics
4Runtime APIs/api/* (or /_/api/* with RUNTIME_API_PREFIX)
5plugin.server.fetchDirect plugin handler
6plugin.routesHono mounted at plugin.base, sorted by specificity (longest path first)
7Plugin appsWorker pool (z-frame iframes)
8Worker apps/:app/* in workerDirs
9Homepage fallbackTries to serve from homepage.app
10404Text Buntime v{version} or shell 404

shouldRouteToShell(req) decides whether navigation goes to the shell (cpanel):

ConditionResult
Sec-Fetch-Mode !== "navigate"Reject (fetch/XHR does not go through the shell)
Path contains /api/Reject
Path is / or emptyAccept
Path matches plugin.baseAccept

Runs after onRequest, allowing auth to be processed before the routing decision.

Workers live in workerDirs in two formats:

# Flat
apps/my-app@1.0.0/
# Nested
apps/my-app/1.0.0/

Version resolution uses semver:

RequestResolves to
/my-app/*latest if it exists, otherwise highest version
/my-app@1/*Highest 1.x.x
/my-app@1.0/*Highest 1.0.x
/my-app@1.0.0/*Exact version
/my-app@^1.0.0/*Semver range
/my-app@latest/*Literal latest directory

getEntrypoint(appDir, manifestEntry?) applies priority:

  1. entrypoint from manifest.yaml.
  2. Auto-discovery: index.htmlindex.tsindex.jsindex.mjs.
TypestaticExecution
index.htmltrueserveStatic + <base href> injection
index.{ts,js,mjs}falseLoaded as worker, runs fetch() or routes

serveStatic validates path traversal (resolve() must stay within baseDir) and falls back to entrypoint for SPA routing.

When a homepage = { app, base: "/" } is configured, requests that return 404 from workers attempt to serve from the homepage app. Useful for SPAs at the root that need to load chunks with arbitrary paths.

External plugins cannot occupy:

  • /api
  • /health
  • /.well-known

Plugin base paths must match /[a-zA-Z0-9_-]+.

RouteMethodDescription
/api/healthGETGeneral health
/api/health/readyGETReadiness probe (k8s)
/api/health/liveGETLiveness probe (k8s)
/api/workersGETList workers in workerDirs
/api/workers/uploadPOSTUpload tarball/zip
/api/workers/:scope/:name[/:version]DELETERemove worker/version
/api/pluginsGETList plugins on the filesystem
/api/plugins/loadedGETList loaded plugins
/api/plugins/reloadPOSTRe-scan and reload
/api/plugins/uploadPOSTUpload a plugin
/api/plugins/:nameDELETERemove a plugin
/api/admin/sessionGETValidates X-API-Key, returns permissions
/api/keysGET/POSTList/create API keys
/api/keys/:idDELETERevoke a key
/api/openapi.jsonGETOpenAPI 3.1 spec
/api/docsGETScalar UI

Full details in the Runtime API Reference.

VariableDefaultDescription
PORT8000HTTP port
NODE_ENVdevelopmentdevelopment | production | staging | test
RUNTIME_WORKER_DIRSrequiredApp directories (PATH style, :)
RUNTIME_PLUGIN_DIRS./pluginsPlugin directories
RUNTIME_POOL_SIZEenv-basedMaximum pool size
RUNTIME_EPHEMERAL_CONCURRENCY2Maximum concurrency for ttl: 0
RUNTIME_EPHEMERAL_QUEUE_LIMIT100Maximum queue for ttl: 0 before 503
RUNTIME_WORKER_CONFIG_CACHE_TTL_MS1000Manifest cache TTL
RUNTIME_WORKER_RESOLVER_CACHE_TTL_MS1000Resolver cache TTL
RUNTIME_LOG_LEVELinfo (prod) / debug (dev)Log level
RUNTIME_API_PREFIX(empty)Moves internal API: ""/api, "/_"/_/api
RUNTIME_ROOT_KEY(optional)Bootstrap root key (synthetic root principal, full access)
RUNTIME_STATE_DIR(optional)Where to store api-keys.db (bun:sqlite)
DELAY_MS100Delay before terminating a worker

The full table — including core-plugin variables — lives in Operations → Environment variables.

  1. Main thread orchestrates, never executes app code. Worker crashes do not bring down the runtime.
  2. Workers enforce isolation — separate heap, modules, and env per instance.
  3. Plugin pipeline intercepts request/response without coupling plugins to each other.
  4. Base-path injection enables SPAs under subpaths without reconfiguring bundlers.
  5. Topological sort orders plugins by dependencies before onInit.