Node servers
javascript/src/server runs modules in Node 25 or later with built-ins only (node:sqlite,
node:crypto, node:worker_threads, fetch). Everything is fetch-style (Request in, Response
out), so any framework mounts it; examples/node-hono uses Hono.
Run a module
createNodeContext gives a state dir (sealed secrets, the records ledger) and a wasm runner.
hostCall({ ctx }) with no session makes public reads only.
import { localModule, runModule } from "./javascript/src/run-module/index.js";
import { createNodeContext, fileFetch, hostCall } from "./javascript/src/server/index.js";
const ctx = createNodeContext({ id: "my-server", stateDir: ".state/my-server" });
const root = `file://${process.cwd()}/`;
const module = await localModule(new URL("target/wasm32-unknown-unknown/release/compose.wasm", root), new URL("examples/rust/compose/module.json", root), fileFetch);
const ran = await runModule(ctx, module, "lookup", { n: 21 }, hostCall({ ctx }));
console.log(JSON.stringify(ran));
{"entry":"lookup","result":{"ack":null,"caller":null,"doubled":42},"events":1,"calls":[]}
A published module: loadModule(ctx, "bobvmbuilder.bsky.social/hello") (or <did>/<name>, an at:// URI; @<pin> checks it).
From the command line
The Node CLI runs one module, from a record or a local build, and prints the same JSON as atlibs run.
node javascript/src/server/cli.mjs run target/wasm32-unknown-unknown/release/compose.wasm examples/rust/compose/module.json lookup '{"n":2}'
"doubled": 4
It signs in only with an app password (--app-password, Server sessions); for a user's
OAuth session use atlibs login and atlibs run --as.
A Hono server
examples/node-hono/server.mjs serves staged modules: each session's OAuth client metadata, user
sign-in, and POST /run/<module>/<entry>. Install its two dependencies once (and those of the optional
sign-in, javascript/src/oauth-server):
(cd examples/node-hono && npm install)
(cd javascript/src/oauth-server && npm install)
Stage the modules, start it, run one:
atlibs stage --target server --out staged examples/rust/compose examples/rust/statusphere > /dev/null
ATLIBS_STAGE=staged/stage.json node examples/node-hono/server.mjs &
curl -s -X POST localhost:8788/run/compose/lookup -H 'content-type: application/json' -d '{"n":2}'; echo
curl -s localhost:8788/oauth/statusphere/client-metadata.json | jq -r .client_id
{"entry":"lookup","result":{"ack":null,"caller":null,"doubled":4},"events":1,"calls":[]}
http://localhost:8788/oauth/statusphere/client-metadata.json
PORT, ATLIBS_STAGE, ATLIBS_SERVER_STATE and ATLIBS_ORIGIN (the public origin) configure it.
It listens on 127.0.0.1; put a reverse proxy or tunnel in front (Server sessions).
Mount the client metadata
clientMetadataHandler(stage, { origin }) answers GET <path><session>/client-metadata.json and
returns undefined for anything else, so your routes still run.
examples/node-hono/server.mjs
const metadata = clientMetadataHandler(stage, { origin });
const app = new Hono();
…
app.all("/oauth/*", async (c) => {
const doc = c.req.method === "GET" && metadata(c.req.raw);
if (doc) return doc;
With ATLIBS_ORIGIN and a session record that declares app, it also serves the app's own identity and
read-only repo first (appRepoHandler, App identity).
Run a module per request
Each request runs as its signed-in user (or the server's own account, or signed out), held to the
module's own session scope by sessionCall.
examples/node-hono/server.mjs
// who this run acts as: the request's user with their own session, else the server's default
let session;
try {
session = await oauth?.sessionOf(c.req.raw);
} catch (error) {
return c.json({ error: "AuthRequired", message: error.message }, error.status ?? 401);
}
const wasm = await realpath(join(dir, staged.wasm));
// id: the wasm's path, as cli.mjs gives a local module (the same ledger owner in a shared state dir)
const module = { ...(await localModule(pathToFileURL(wasm), pathToFileURL(join(dir, staged.spec)), fileFetch)), id: wasm };
const runDid = session?.did ?? did;
// signed in (a user, or the operator account): only what the module's own session allows (stage.json is the policy)
const runCall = runDid ? sessionCall(stage, c.req.param("module"), session ? hostCall({ ctx, xrpc: session.xrpc, pds: session.pds }) : call) : call;
return c.json(await runModule(ctx, module, c.req.param("entry"), { ...(runDid && { did: runDid }), ...input }, runCall, { provides: [] }));
Refuse cross-site runs
runRefusal is the same guard atlibs serve applies: a Host that is loopback or the configured
origin's, no foreign Origin, and content-type: application/json (which a cross-site form cannot send
without a preflight).
curl -s -X POST localhost:8788/run/compose/lookup -H 'Origin: https://evil.example' -H 'content-type: application/json' -d '{"n":2}'; echo
{"error":"Forbidden","message":"Origin https://evil.example may not run modules here"}
Accept calls from modules: service auth
A module calling your XRPC service with service: "https://you.example" sends a service-auth JWT.
verifyServiceAuth returns the caller's DID or throws (answer 401); it checks the audience, method,
expiry, the issuer's signing key, and replays.
import { createDidKey } from "./javascript/src/didkey/index.js";
import { verifyServiceAuth } from "./javascript/src/serviceauth/index.js";
// a caller: a did:key mints a token for one method at one audience
const caller = await createDidKey();
const token = await caller.serviceAuth({ aud: "did:web:you.example", lxm: "com.example.hello" });
// your route; resolve is only here to skip the network for a did:key
const resolve = async () => ({ verificationMethod: [{ id: "#atproto", publicKeyMultibase: caller.did.slice("did:key:".length) }] });
const who = await verifyServiceAuth({ audiences: ["did:web:you.example"] }, `Bearer ${token}`, "com.example.hello", { resolve });
console.log(who === caller.did);
await verifyServiceAuth({ audiences: ["did:web:you.example"] }, `Bearer ${token}`, "com.example.hello", { resolve }).catch((e) => console.log(e.message));
true
the token was already used
Test it
node --test examples/node-hono/test.mjs 2>&1 | grep -E '^ℹ (pass|fail)'
ℹ pass …
ℹ fail 0
Previous: Sign in from a page | Next: Rust servers