Configuration

bext-lite has three configuration surfaces, and they are cleanly separated:

  1. bext.config.toml - the PRISM app config that describes the site the exporter compiles. This is the same file the full bext server reads.
  2. The dist-lite manifest - what the exporter emits and pins into the bundle: the route table, the host-ABI contract, and the sandbox policy the runtime honors.
  3. Environment variables - the runtime knobs (worker pool, timeouts, body caps) and the exporter knobs (React toolchain, degraded fallback).

Serve-time behavior beyond these knobs is deliberately fixed - bext-lite is small by design. See Architecture for the layers.

The app config: bext.config.toml#

The exporter points at a site's source directory and reads its bext.config.toml to find the app and how it renders. A minimal PRISM app config:

[server]
app_dir = "."          # the PRISM app source root (where src/app lives)
static_dir = "public"  # copied verbatim into dist-lite/public
port = 3021            # dev port for the full server; the lite CLI takes --port

[framework]
type = "prism"         # bext-lite exports PRISM sites

[build]
watch_dirs = ["src/app", "src/components", "src/lib", "content"]
live_reload = true

[build.css]
route_css = true       # per-route CSS extraction

[rendering]
mode = "isr"           # a full-server hint; bext-lite always serves buffered
revalidate = 3600
  • [server].app_dir is the source root the exporter discovers routes, API routes, actions, and islands under. static_dir (default public) is copied into dist-lite/public and served directly.
  • [framework].type must be prism.
  • [build].watch_dirs drives bext-lite mobile dev's re-export-on-change live-reload loop. live_reload = true is what makes that loop fire.
  • [rendering] and [perf.budget] are hints the full multi-tenant server acts on. bext-lite renders each route fully and then serves it - buffered, not streamed - so these do not change what serve does. They are safe to keep so one config drives every target.
Note

The exporter materializes the current wrapper for every route from source, so a site does not need to have been served by the full server first. What you test on the server is byte-identical to what every target ships. See Deploying.

The dist-lite manifest#

Every export writes dist-lite/manifest.json. It is the contract between the bundle and the host: the runtime reads it before touching any route code. The top-level shape:

{
  "bextLiteManifest": 1,
  "site": "device-playground",
  "generatedBy": "bext-lite-build",
  "runtime": { "engine": "quickjs|v8", "renderEntry": "__bextPrismRender", "apiEntry": "__bextApiHandler" },
  "runtimeAbi": {
    "abi": 1,
    "globals": ["__bextPrismRender", "__bextApiHandler", "__httpFetch"],
    "polyfillsVersion": "a1b2c3d4e5f60718",
    "bunVersion": "1.x.y",     // present only when a React toolchain built the bundle
    "reactVersion": "19.x.y"
  },
  "reactBase": "react-base.js", // null when the site has no "use client" pages
  "routes":    [ /* page bundles */ ],
  "apiRoutes": [ /* api bundles */ ],
  "actions":   [ /* server-action bundles */ ],
  "islands":   [ /* compiled "use client" / "use signals" islands */ ],
  "public":    "public"
}

Each routes[] entry carries urlPath, key, bundle (the relative JS path), params, isDynamic, isCatchAll, and isClient. A renderer field is present only when it is meaningful: "react" for an AOT-compiled "use client" page, or "degraded" for a string-builder fallback (see React); plain string-builder pages carry no renderer.

What runtimeAbi pins, and why#

runtimeAbi is the load-time gate. Before the runtime evaluates a single line of route code - and before OTA installs a downloaded bundle - it compares the manifest's abi against the version this build implements and refuses a mismatch. That is what stops a bundle and a host built against different contracts from silently drifting.

Field Meaning Gated?
abi Host-bundle contract version. A different value hard-stops the load. Yes - the load/OTA gate. Absent = allowed (pre-ABI bundles).
globals The host globals the bundle references by name (__bextPrismRender, __bextApiHandler, __httpFetch). Documentary.
polyfillsVersion First 16 hex of a sha256 of the runtime polyfills the bundle was built against. Diagnostic - flags a mismatched polyfill set, not gated.
bunVersion / reactVersion The toolchain that SSR-built the React bundles. Diagnostic - present only for React sites.

reactBase names the shared React runtime bundle the runtime evals before any React route's own bundle; it is null for string-builder-only sites. A published release also carries the runtimeAbi pin, so OTA refuses a correctly-signed bundle built for a different ABI - authenticity is not host compatibility.

Optional sandbox fields#

Two manifest fields tune the sandbox. The runtime honors them, but the exporter does not emit them by default - add them to manifest.json (or via your orchestration) when you need them:

Field Effect Default
egress "all" or "unrestricted" opts out of the SSRF egress guard for a trusted single-app deployment that must reach LAN or private services. guard on
envAllowlist An array of env-var names the bundle may read through __env; anything else reads as absent. all readable

A published bundle also gets a version string (from bext-lite publish --version); a freshly exported bundle has none and reads as "baseline".

Environment variables#

Runtime (the serve / embedded host)#

All read once at boot. A value of 0 where noted disables that guard.

Variable Default Effect
BEXT_LITE_SERVER_WORKERS 8 Steady HTTP worker threads (clamped to >= 1).
BEXT_LITE_SERVER_MAX_CONNS 64 Hard cap on total live handlers (steady + overflow). Clamped to at least 2 x workers so a re-entrant loopback /api fetch can always be admitted.
BEXT_LITE_SERVER_READ_TIMEOUT_MS 10000 Per-socket read timeout (slowloris guard). 0 disables.
BEXT_LITE_MAX_BODY_BYTES 33554432 (32 MiB) Max request body. A larger declared Content-Length is refused 413 before any allocation.
BEXT_LITE_RENDER_DEADLINE_MS 10000 Wall-clock budget for one JS execution (module init, page render, API dispatch). A wedged synchronous loop is interrupted and the runtime survives. 0 disables.
BEXT_LITE_NATIVE_TIMEOUT_MS 30000 Wall-clock budget for one native-bridge call (camera, biometrics). On expiry the worker gets 504 and is freed. 0 calls inline, unbounded.
BEXT_LITE_NATIVE_MAX_INFLIGHT 16 Max concurrently-outstanding native-bridge call threads. At the cap a new call returns 503 instead of spawning another leakable thread (clamped to >= 1).
BEXT_LITE_UPDATE_ON_LAUNCH off When OTA is configured, run a best-effort update check before serving. Truthy (1/true/yes/on) enables.
Note

Each render runs in an isolate with a fixed 512 MB memory limit and a 16 MB stack. These are hard-coded caps, not env knobs.

OTA is otherwise configured by CLI flags, not environment variables: bext-lite serve <dist> --update-url URL --pubkey HEX [--data-dir D]. --update-url requires --pubkey (an unsigned bundle is refused). --data-dir defaults to <dist>/.bext-lite-data. Full flow in OTA and CLI reference.

Exporter (bext-lite-build)#

Variable Effect
BEXT_LITE_VENDOR_NODE_MODULES Path to a node_modules holding react / react-dom / scheduler for the React SSR build. Only needed when the app has "use client" pages and does not vendor its own React.
BEXT_SHARED_FRAMEWORK_DIR Path to the shared @bext-stack/framework, so an app that imports it resolves at export time.
BEXT_LITE_BUN Override the bun binary used for the React build (default: bun on PATH).
BEXT_LITE_ALLOW_DEGRADED_CLIENT 1/true/yes/on: if a React page cannot be compiled, ship a string-builder fallback tagged renderer:"degraded" instead of failing the export. Off by default (a broken React bundle fails the build). See React.

A string-builder-only app needs none of these - no bun, no React, no env.

Untrusted-tenant orchestrator#

When bext-lite hosts untrusted tenants (one instance per tenant, behind the front router), the orchestrator applies OS isolation and cgroup caps. These knobs tune it - see Security:

Variable Default Effect
BEXT_LITE_MEMORY_MAX_MB 768 Per-instance cgroup v2 memory.max.
BEXT_LITE_CPU_WEIGHT 100 Per-instance cgroup v2 cpu.weight.
BEXT_LITE_PIDS_MAX 64 Per-instance cgroup v2 pids.max.
BEXT_LITE_SECCOMP on The seccomp syscall denylist. BEXT_LITE_SECCOMP=0 disables it.
BEXT_LITE_RUN_DIR /var/run/bext-lite Where per-instance pidfiles live so a re-exec'd master can re-attach to running instances.
BEXT_LITE_BIN (resolved) Path to the bext-lite instance binary the orchestrator execs per tenant.

Secrets#

The secrets store resolves from the environment, in order: BEXT_SECRET_<APPID>_<NAME>, then BEXT_SECRET_<NAME>, then <NAME> (app id upper-cased with - mapped to _). Set them where the runtime process can see them; the app reads them by name. See On-device data.