Configuration
bext-lite has three configuration surfaces, and they are cleanly separated:
bext.config.toml- the PRISM app config that describes the site the exporter compiles. This is the same file the full bext server reads.- The
dist-litemanifest - what the exporter emits and pins into the bundle: the route table, the host-ABI contract, and the sandbox policy the runtime honors. - 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_diris the source root the exporter discovers routes, API routes, actions, and islands under.static_dir(defaultpublic) is copied intodist-lite/publicand served directly.[framework].typemust beprism.[build].watch_dirsdrivesbext-lite mobile dev's re-export-on-change live-reload loop.live_reload = trueis 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 whatservedoes. They are safe to keep so one config drives every target.
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. |
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.
Related#
- CLI reference - the flags each command takes
- Architecture - what the manifest describes
- Security - the sandbox the manifest fields configure
- Over-the-air updates - how a signed bundle carries the runtimeAbi pin