Custom HTTP Routes
The LoopbackServer surface is mount / mountDir / unmount for file
mounts, plus registerRoute for arbitrary HTTP endpoints — a dev-only
upload route, a status page, or any custom API.
registerRoute
Section titled “registerRoute”registerRoute(method, path, handler) registers an exact METHOD path
route. The handler receives the raw (req, res); read a POST body with
collectRequestBody(req, maxBytes), always passing a byte cap:
import { collectRequestBody, type WorkletRuntime } from '@ekrooh/bare/runtime';
declare const runtime: WorkletRuntime;
runtime.server.registerRoute('POST', '/upload', async (req, res) => { const body = await collectRequestBody(req, 1024 * 1024); // rejects past 1 MiB res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ received: body.length }));});- Routes run before mount resolution, after the auth gate: in device
mode the request must carry the session cookie/token (401 otherwise) and
pass the
Sec-Fetch-Sitecheck (403 forsame-site/cross-site); dev mode is open, but the origin rule still applies. - The path is normalized like a mount path (percent-decoded, trailing slash
dropped), so
/api/and/a%20bare reachable; a path no request could match throws. The method is upper-cased at registration. - Async handlers are supported. A handler that throws or rejects is answered
with a bare
500 Internal Server Error; the cause goes to the log. - The response starts with
Referrer-Policy: no-referrerandX-Content-Type-Options: nosniffalready set, plusCross-Origin-Resource-Policy: same-originwhenever auth is on (off in dev). Set your ownContent-Type. - A handler that fails after its response has ended is only logged. The 500
for a failed route never carries headers the handler had set (such as
Set-Cookie).
File mounts
Section titled “File mounts”For serving static bytes, the mount API still applies:
import type { WorkletRuntime } from '@ekrooh/bare/runtime';
declare const runtime: WorkletRuntime;
runtime.server.mount('/photos/1.jpg', '/absolute/path/to/photo.jpg');runtime.server.mountDir('/photos', '/absolute/path/to/photos');mountDir serves every file below the prefix (longest-prefix match). By
default a directory mount is gated like a file mount. Pass
{ public: true } to mark it as world-readable bootstrap content (the web app
at / is mounted that way); only a public mount applies the SPA fallback to
index.html for unknown GET navigations — a non-public mount 404s them.
Files from a gated mount are served as content, not app code: they carry the
same sandbox Content-Security-Policy as file mounts, so an HTML or SVG file in
a mounted folder (user files, a peer-synced folder) never runs script in the
app origin. Ship app code only in the public mount.
A gated directory mount must not be inside (or the same folder as) the public
one: mountDir throws on that layout, in either mount order (real paths;
case- and normalization-insensitive on macOS, iOS and Windows). A gated folder
strictly around the web app folder is allowed. Keep gated file mounts out of the public web
app directory too; that layout is not detected, and such a file would be
reachable before login through its public URL.
Bytes from the browser → worklet
Section titled “Bytes from the browser → worklet”For chunked uploads of large files (photos), the wire protocol’s
DISPATCH/INVOKE envelopes remain the supported channel. A registerRoute
endpoint gives you a second path (e.g. dev-only) when you need browser →
worklet bytes without the protocol — pair it with mount/mountDir to serve
what was uploaded.