Deploying
Every target starts from the same artifact: a self-contained dist-lite/ produced
by the AOT exporter. What changes per target is the shell that serves it.
Export the bundle#
Point the exporter at any PRISM site's source. It compiles every route, API route,
action, and island, and emits a complete dist-lite/:
bext-lite-build ./sites/my-app -o ./dist-lite
There is no "serve once first" precondition - the exporter materializes the current wrapper for every route from source, creating never-served routes and refreshing stale ones. The output is byte-identical to what the bext server renders for the same routes.
Standalone binary#
The simplest target: run the bundle from a single binary.
bext-lite serve ./dist-lite [--port 8080]
That is the whole deploy. No Node, no V8 cage, no install step. The binary is a
v8-free runtime that renders on QuickJS, serves static assets from public/,
matches dynamic routes, runs /api/* handlers, and returns 404 for misses. It
cross-compiles to Linux, macOS, and Windows.
Pages use a warm-context pool - the polyfills and the route bundle are evaluated
once per route and reused, so after the first hit a render runs only
__bextPrismRender. A cold render of a real page is a few milliseconds; a warm one
is about 1 ms.
Embedding via spawn_server#
A shell embeds the runtime in-process rather than launching the CLI. The embedding primitive is one call:
// Boot the runtime on a background thread; returns the bound port.
let port = bext_lite::spawn_server(std::path::Path::new("./dist-lite"), "127.0.0.1", 0)?;
// then open a WebView at http://127.0.0.1:{port}/
Pass 0 for the port to let the OS pick one and read back the bound value.
spawn_server_with_ota is the same call with an OTA config attached,
so the embedding shell can offer "Check for updates."
Desktop shell#
bext-lite-desktop is a Tauri crate that embeds the runtime and opens a native
WebView at its localhost port. It builds two ways:
- Headless (default) - embeds the runtime and serves it, with no system WebView dependency, so the integration is CI-verifiable.
--features gui- bootsspawn_serverand points aWebviewWindowat the bound port.dist-lite/ships as a Tauri resource, resolved at launch.
Package it with the standard Tauri toolchain:
cargo tauri icon
cargo tauri build --features gui
# -> .dmg / .msi / .deb / AppImage
Code signing, notarization, and the auto-update channel come from Tauri. An installed desktop app is roughly 10-20 MB.
The GUI build needs the WebView toolchain (webkit2gtk on Linux) and a display, so it is built on a desktop machine, not in a headless sandbox. Everything that does not need a display - the embeddable runtime, the in-process host, KV/API through the desktop binary - is verified headless.
Mobile#
The bext-lite mobile subcommands scaffold and drive the iOS/Android shells. They
orchestrate the two tools that already exist - bext-lite-build to export, then
cargo tauri ios/android to build - rather than reimplementing a build.
bext-lite mobile init # scaffold ios/ + android/ shell projects (once)
bext-lite mobile run ios # export -> bundle -> build -> run on simulator/device
bext-lite mobile run android
bext-lite mobile build # produce .ipa / .aab
bext-lite mobile dev # live-reload: re-export on change, hot-swap the bundle
Start with the static model: AOT-render every route at build (on V8), ship HTML +
island JS + a seeded SQLite, and hydrate on device with the free client runtime.
It needs no engine on the device for rendering and de-risks packaging, store
review, and the native bridge before you take on on-device SSR. On-device
rendering (QuickJS embedded in the shell, exactly like serve) is the later,
fuller cut. Native device APIs (camera, biometrics, geolocation, secure storage,
haptics, share, notifications) reach the app through the bext.device.* bridge on
the same SDK channel. See the mobile RFC for the runbook and the Mac/NDK toolchain
requirements.
Device builds need a Mac (Xcode) and/or the Android NDK - they cannot be produced in a headless sandbox. The pure-Rust/JS substrate (the native bridge, the event ring, the CLI, the shell template) is host-testable; the real Swift/Kotlin implementations and the device build are done on a Mac/NDK box.
The bounded worker pool#
Standalone, the server accepts connections into a fixed pool of worker threads
(BEXT_LITE_SERVER_WORKERS, default 8) reading a zero-capacity rendezvous channel,
plus a bounded overflow lane. The rendezvous is load-bearing: a page or API render
that await fetch('/api/...')s makes a real blocking loopback HTTP call that needs
a second handler to accept it, so the acceptor must learn immediately when no
worker is free and divert to the overflow lane rather than let the render deadlock
the pool. Total live handlers are capped at BEXT_LITE_SERVER_MAX_CONNS
(default 64) with brief backpressure at the cap, instead of an unbounded
thread-per-connection model - which matters on a phone, where memory and
background CPU are constrained and a blocking native call (a camera prompt can take
seconds) parks a handler.
Other guards, all read once from the environment:
| Variable | Default | Effect |
|---|---|---|
BEXT_LITE_SERVER_WORKERS |
8 | Steady worker threads |
BEXT_LITE_SERVER_MAX_CONNS |
64 | Hard cap on total live handlers (>= 2x workers) |
BEXT_LITE_SERVER_READ_TIMEOUT_MS |
10000 | Per-socket read timeout (slowloris guard); 0 disables |
BEXT_LITE_MAX_BODY_BYTES |
33554432 | 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; a wedged synchronous loop is interrupted and the runtime survives; 0 disables |
Footprint#
The v8-free runtime binary is about 6 MB (with rusqlite and a pure-Rust TLS client). A whole app to ship is that binary plus its assets - on the order of a few megabytes, versus Electron at 150 MB and up. An installed desktop app is roughly 10-20 MB. On mobile the runtime plus the bundle sits well under typical app budgets.
Related#
- Architecture - what
dist-lite/contains - The engine decision - which engine each target runs
- Over-the-air updates - updating a shipped app without a rebuild
- On-device data - where each target puts its data