Architecture
bext-lite runs a single PRISM app outside the multi-tenant server. Where the masquerade is a 124 MB Rust binary with V8 isolate pools, a WAF, TLS, and vhost routing, bext-lite is a tiny single-tenant runtime that takes a precompiled PRISM app and serves it. Same PRISM source, a swappable runtime.
The design splits into four layers. Two of them are fully portable, one is re-implemented per target from code that already exists, and the client side is free everywhere. That is what lets you write the app once and ship it as a standalone binary, a desktop app, an edge function, a browser app, a mobile app, or an embedded box.
1. COMPILE ts-rs transpile + turbopack bundle -> CJS build-time, AOT
output references host globals by NAME ship JS, no compiler on device
------------------------------------------------------------
2. RENDER JS engine evals bundle, calls __bextPrismRender the engine decision: V8 vs QuickJS
+ ~122 KB of pure-JS, engine-agnostic polyfills
------------------------------------------------------------
3. HOST Rust impls of the bridge functions: per-target: native fs/sqlite/http,
fs, http, sqlite (KV + DB), storage, env, log or edge KV/D1/fetch, or a mobile bridge
------------------------------------------------------------
4. SHELL routing + serve. a tiny single-app router, or a Tauri WebView,
or a WebView, or an edge fetch handler or an edge fetch handler
CLIENT (browser / WebView): island loader + per-island JS - already pure browser JS, runs everywhere.
1. Compile#
At build time, bext-lite-build discovers a site's routes, API routes, actions,
and islands, then compiles each one through the exact same path the server uses:
ts-rs transpiles the TSX, and turbopack AOT-bundles each route into a CommonJS
bundle with its imports inlined. The bundle does not import a host module; it
references the host by name - __bextPrismRender, __httpFetch, __env, and the
rest are plain global identifiers the runtime provides.
The output is a self-contained dist-lite/:
dist-lite/
manifest.json route table + runtimeAbi pin
routes/<key>.js page bundles (define __bextPrismRender)
api/<key>.js api bundles (define __bextApiHandler)
actions/<stem>.js server-action bundles
islands/<name>.js compiled "use client" / "use signals" islands
public/ copied static assets (incl. public/islands/*.js)
You ship JavaScript. There is no compiler on the device.
The exported bundles are byte-identical to what the bext server produces for the same routes - verified by sha256. What you test on the server is what every target ships. See Deploying for the export command.
2. Render#
A JS engine evaluates the route bundle plus about 122 KB of pure-JS,
engine-agnostic polyfills (Intl, Buffer, TextEncoder, crypto, URL, Blob/File,
Request/Response, ReadableStream, timers, process, console, and the rest),
then calls __bextPrismRender(props) and gets HTML back. The render contract is a
pure function of props to a string.
The polyfills are the same code regardless of engine, so this half of the layer ports unchanged. The engine underneath does not - that is the one load-bearing choice in the whole system, covered in The engine decision. In short: V8 on desktop and server, QuickJS everywhere else (iOS, edge, browser, embedded). The output is byte-identical across both.
3. Host#
The bundle's host-global references have to resolve to something. Layer 3 is the set of Rust implementations behind them - about 18 functions covering the filesystem, HTTP, SQLite (KV and a per-app database), object storage, environment, and logging.
This is the layer re-implemented per target, but the native implementations
already exist and are copied from the bext core. On a laptop or a phone they map
to real files, rusqlite, and a blocking HTTP client. On the edge they would map
to Workers KV, D1, and the platform fetch. See On-device data for
what the host exposes to your app and how to reach it.
4. Shell#
The shell does routing and serving. The masquerade does this multi-tenant with nginx-compat and vhosts; bext-lite needs only a single-app router. Standalone, that is a tiny hand-rolled HTTP/1.1 server with a bounded worker pool. Embedded in a desktop or mobile app, the shell is a Tauri/native WebView pointed at the runtime's localhost port. At the edge, it is a platform fetch handler. No WAF, no TLS by default, no multi-tenancy - small by design.
What is portable#
| Layer | Portability |
|---|---|
| 1. Compile (after AOT) | Fully portable - the bundle is plain JS |
| 2. Render polyfills | Fully portable - engine-agnostic pure JS |
| 2. Render engine | The per-target decision (V8 / QuickJS / QuickJS-on-WASM) |
| 3. Host | Re-implemented per target, but native impls are copyable |
| 4. Shell | Small per target |
| Client (islands) | Free on every target - already pure browser JS |
The island hydration runtime that runs in the browser or WebView is pure browser JS with zero server coupling, so it runs unchanged on every target at no extra cost. That is why the mobile "static export + islands + local SQLite" model can sidestep on-device rendering almost entirely: ship the HTML, hydrate with the free client runtime, and route data through the host bridge.
One bundle, every target#
The manifest.json carries a runtimeAbi pin - the ABI version, the required
host globals, and a hash of the polyfills the bundle was built against. The
runtime refuses a bundle built for a different ABI before it touches any code, so
a bundle and a host that were built against different contracts never silently
mismatch. Within one ABI, the same dist-lite/ runs on any shell with any engine,
and the rendered HTML is identical.
Related#
- The engine decision - why V8 here and QuickJS there
- On-device data - the host data layer
- Deploying - shipping each target
- Over-the-air updates - swapping the bundle without a rebuild