Pular para o conteúdo

Runtime

Runtime modular para Bun com um pool de workers, sistema de plugins e suporte a micro-frontends. O processo principal orquestra requisições, mas nunca executa código de aplicação — esse trabalho fica isolado nos workers (veja Worker Pool).

CamadaTecnologia
RuntimeBun (Bun.serve, Worker, Bun.file)
Framework HTTPHono
ValidaçãoZod
Cache LRUquick-lru
Versionamentosemver
Documentação da APIhono-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

A inicialização acontece em camadas, cada uma dependendo da anterior:

PassoMóduloResponsabilidade
1constants.tsValida PORT, NODE_ENV, DELAY_MS; define IS_DEV, IS_COMPILED
2config.tsResolve RUNTIME_WORKER_DIRS (obrigatório), RUNTIME_PLUGIN_DIRS, RUNTIME_POOL_SIZE
3loader.tsVarre pluginDirs, lê manifest.yaml, filtra enabled, ordena por dependências
4api.tsCria o logger, WorkerPool, PluginRegistry, monta as rotas core e o app Hono
5index.tsInicia Bun.serve, executa runOnServerStart, registra o handler de SIGINT
AspectoDesenvolvimentoProdução
poolSize10500
Loggerpretty (colorido)json (estruturado)
Nível de logdebuginfo
HMRHabilitadoDesabilitado

Outros padrões: staging = 50 workers, test = 5.

Bun.serve é configurado em index.ts com algumas particularidades operacionais:

OpçãoValorMotivo
idleTimeout0Desabilita o timeout para que conexões SSE/WebSocket permaneçam abertas
routes["/favicon.ico"]204 No ContentEvita 404s nos logs
routespluginRoutesserver.routes agregadas dos plugins
development.hmrtrue (dev)Hot Module Replacement
websocketcombinadoHandler único que agrega todos os plugins

SIGINT dispara um pipeline com timeout total de 30s (SHUTDOWN_TIMEOUT_MS):

  1. Arma um temporizador de saída forçada (process.exit(1) em 30s).
  2. registry.runOnShutdown() — hooks de plugins em ordem reversa (LIFO).
  3. pool.shutdown() — termina todos os workers.
  4. logger.flush().
  5. clearTimeout + process.exit(0).

Qualquer falha na cadeia cai no bloco catch e força o código de saída 1.

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

Aplicado a /api/* para métodos que alteram estado (POST, PUT, PATCH, DELETE):

CondiçãoComportamento
Método em [GET, HEAD, OPTIONS]Ignora (bypass)
Header X-Buntime-Internal: trueIgnora (bypass) (worker → runtime)
Sec-Fetch-Mode presente sem Origin403
Origin.host !== request.host403

Constantes em constants.ts: DEFAULT = 10MB, MAX = 100MB. Configuráveis via env (BODY_SIZE_DEFAULT, BODY_SIZE_MAX) e por worker no manifest.yaml (maxBodySize: 50mb). Se maxBodySize > MAX, o runtime emite um aviso e usa MAX.

A validação acontece em duas etapas:

  1. Caminho rápido: Content-Length inválido ou maior que o limite → 413 Payload Too Large.
  2. Caminho lento (chunked): leitura completa, reverificação do tamanho real.

Tudo retorna BodyTooLargeError no código de aplicação. A resposta inclui o header X-Request-Id para correlação de logs.

rewriteUrl(url, basePath) remove o prefixo do caminho preservando a query string — usado antes de injetar no worker. A função assume que o caminho começa com basePath (validado na camada anterior).

EntradaResultado
basePath = ""Retorna o pathname original
pathname === basePathRetorna "/"
pathname não começa com basePathComportamento indefinido — valide na camada anterior
HeaderDireçãoDescrição
X-Baseruntime → workerCaminho base injetado para SPAs
X-Buntime-Internalworker → runtimeIgnora o CSRF
X-Not-Foundruntime → shellSinaliza renderização consistente de 404
X-Request-IdbidirecionalUUID de correlação

A resolução em app.ts segue uma ordem de prioridade estrita. Rotas mais específicas (plugins) têm precedência sobre as genéricas (workers):

OrdemCamadaExemplo
1CSRFBloqueia antes de tudo
2Modo app-shellshouldRouteToShell() intercepta a navegação
3Hooks onRequestAuth, rate limiting, métricas
4APIs do runtime/api/* (ou /_/api/* com RUNTIME_API_PREFIX)
5plugin.server.fetchHandler direto do plugin
6plugin.routesHono montado em plugin.base, ordenado por especificidade (caminho mais longo primeiro)
7Apps de pluginWorker pool (iframes z-frame)
8Apps de worker/:app/* em workerDirs
9Fallback da homepageTenta servir a partir de homepage.app
10404Texto Buntime v{version} ou 404 do shell

shouldRouteToShell(req) decide se a navegação vai para o shell (cpanel):

CondiçãoResultado
Sec-Fetch-Mode !== "navigate"Rejeita (fetch/XHR não passa pelo shell)
Caminho contém /api/Rejeita
Caminho é / ou vazioAceita
Caminho corresponde a plugin.baseAceita

Roda depois de onRequest, permitindo que a auth seja processada antes da decisão de roteamento.

Workers ficam em workerDirs em dois formatos:

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

A resolução de versão usa semver:

RequisiçãoResolve para
/my-app/*latest se existir, caso contrário a versão mais alta
/my-app@1/*A maior 1.x.x
/my-app@1.0/*A maior 1.0.x
/my-app@1.0.0/*Versão exata
/my-app@^1.0.0/*Faixa (range) semver
/my-app@latest/*Diretório latest literal

getEntrypoint(appDir, manifestEntry?) aplica a prioridade:

  1. entrypoint do manifest.yaml.
  2. Descoberta automática: index.htmlindex.tsindex.jsindex.mjs.
TipostaticExecução
index.htmltrueserveStatic + injeção de <base href>
index.{ts,js,mjs}falseCarregado como worker, executa fetch() ou routes

serveStatic valida path traversal (resolve() deve permanecer dentro de baseDir) e faz fallback para entrypoint no roteamento de SPA.

Quando uma homepage = { app, base: "/" } está configurada, requisições que retornam 404 dos workers tentam ser servidas pelo app da homepage. Útil para SPAs na raiz que precisam carregar chunks com caminhos arbitrários.

Plugins externos não podem ocupar:

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

Os caminhos base dos plugins devem corresponder a /[a-zA-Z0-9_-]+.

RotaMétodoDescrição
/api/healthGETSaúde geral
/api/health/readyGETReadiness probe (k8s)
/api/health/liveGETLiveness probe (k8s)
/api/workersGETLista workers em workerDirs
/api/workers/uploadPOSTUpload de tarball/zip
/api/workers/:scope/:name[/:version]DELETERemove worker/versão
/api/pluginsGETLista plugins no sistema de arquivos
/api/plugins/loadedGETLista plugins carregados
/api/plugins/reloadPOSTRe-varre e recarrega
/api/plugins/uploadPOSTUpload de um plugin
/api/plugins/:nameDELETERemove um plugin
/api/admin/sessionGETValida X-API-Key, retorna permissões
/api/keysGET/POSTLista/cria chaves de API
/api/keys/:idDELETERevoga uma chave
/api/openapi.jsonGETSpec OpenAPI 3.1
/api/docsGETUI do Scalar

Detalhes completos na Referência da API do Runtime.

VariávelPadrãoDescrição
PORT8000Porta HTTP
NODE_ENVdevelopmentdevelopment | production | staging | test
RUNTIME_WORKER_DIRSobrigatórioDiretórios de apps (estilo PATH, :)
RUNTIME_PLUGIN_DIRS./pluginsDiretórios de plugins
RUNTIME_POOL_SIZEbaseado no envTamanho máximo do pool
RUNTIME_EPHEMERAL_CONCURRENCY2Concorrência máxima para ttl: 0
RUNTIME_EPHEMERAL_QUEUE_LIMIT100Fila máxima para ttl: 0 antes de 503
RUNTIME_WORKER_CONFIG_CACHE_TTL_MS1000TTL do cache do manifest
RUNTIME_WORKER_RESOLVER_CACHE_TTL_MS1000TTL do cache do resolver
RUNTIME_LOG_LEVELinfo (prod) / debug (dev)Nível de log
RUNTIME_API_PREFIX(vazio)Move a API interna: ""/api, "/_"/_/api
RUNTIME_ROOT_KEY(opcional)Chave root de bootstrap (principal root sintético, acesso total)
RUNTIME_STATE_DIR(opcional)Onde armazenar api-keys.db (bun:sqlite)
DELAY_MS100Atraso antes de terminar um worker

A tabela completa — incluindo as variáveis dos core-plugins — fica em Operações → Variáveis de ambiente.

  1. A thread principal orquestra, nunca executa código de app. Crashes de worker não derrubam o runtime.
  2. Workers impõem isolamento — heap, módulos e env separados por instância.
  3. O pipeline de plugins intercepta requisição/resposta sem acoplar os plugins entre si.
  4. Injeção de caminho base (base-path) habilita SPAs em subcaminhos sem reconfigurar bundlers.
  5. Ordenação topológica organiza os plugins por dependências antes de onInit.