How it works
A module is pure WebAssembly: it gets JSON and returns the calls it wants made. The host makes every call with the session it was given, and hands each result back as the next event.
The pieces
flowchart LR
PDS[(publisher's PDS)] -- wasm.module record + blob --> Host
Host -- event JSON --> Module[module.wasm]
Module -- calls, state, result --> Host
Host -- XRPC with the user's session --> UserPDS[(user's PDS, services)]
- Module: a
.wasmwith no imports, and itsmodule.json(entries, allowed calls, session). - Host: runs modules and makes their calls. Three hosts share one protocol: the browser
(
javascript/src), Node (javascript/src/server) and Rust (rust/host, behind theatlibsCLI). - Session: who the calls act as. A module names its session record, and a signed-in run may only make the calls that record's scope allows.
- Records: modules, their OAuth client records, source and compositions are atproto records, so anyone can publish them and anyone can fetch and check them.
The step protocol
The host calls an entry once per event, with the action's input, did, now, the state the entry
returned last and the event. The entry answers with calls to make and new state, or a result.
import { load } from "./rust/src/harness.mjs";
const { run } = await load(new URL("./target/wasm32-unknown-unknown/release/hello.wasm", `file://${process.cwd()}/`));
const input = { did: "did:plc:alice", now: "2026-11-09T00:00:00.000Z", collection: "xyz.statusphere.status" };
console.log(JSON.stringify(run("count", { ...input, state: null, event: { type: "start" } })));
console.log(JSON.stringify(run("count", { ...input, state: null, event: { type: "result", id: "list", nsid: "com.atproto.repo.listRecords", ok: true, output: { records: [{}, {}] } } })));
{"calls":[{"id":"list","nsid":"com.atproto.repo.listRecords","params":{"collection":"xyz.statusphere.status","limit":"100","repo":"did:plc:alice"},"service":"did:plc:alice"}]}
{"done":true,"result":{"collection":"xyz.statusphere.status","records":2}}
Events in:
start: the first event.result {id}: a call finished;okandoutput, orok: falseanderror.message {id},closed {id}: a subscription's messages, and its end.timer {id}: a timer the module set.input {data}: data the host feeds into a running module.callback {id, cap}: another module invoked a capability this run passed it.
Output fields: calls, send (write up a subscription), cancel, timers, state, result,
done. The run ends when the module says done, or when nothing is pending.
Who a run acts as
| where | acts as | chosen by |
|---|---|---|
| a page | the signed-in user's OAuth session | BrowserOAuthClient (Sign in from a page) |
a server (atlibs serve, Node) | each request's signed-in user, with their own OAuth session | /oauth/login, then the atlibs_session cookie or bearer (Server sessions) |
| the CLI | an OAuth session atlibs login saved | --as <handle> |
| tests, an operator account | an app password | --app-password <handle> with --password-file or --password-env |
| none of the above | nobody: public reads only |
Nothing is read from the environment: ATPROTO_* variables mean nothing to atlibs.
Where calls go
| call | goes to |
|---|---|
{ nsid, params } / { nsid, input } | the session's own PDS |
{ nsid, params, proxy: "did:web:svc#id" } | the PDS, which proxies it (atproto-proxy); signed out, a query goes to that service directly, no token |
{ nsid, input, service: "https://svc" } | that service, with a service-auth token the host mints |
{ nsid, params, service: "did:plc:..." } | a public read of that account's PDS, no token (did:web:svc#id: of that service) |
{ nsid, ..., subscribe: true } | a WebSocket the host holds; messages arrive as events |
com.fedfork.exec.* | the host itself: attest, did:key, and transports (iroh, WebRTC) |
Details: Calls.
Records
| collection | what |
|---|---|
com.fedfork.exec.wasm.module | a module: its wasm blob, entries, allowed calls, session, hooks |
com.fedfork.exec.oauth.clientMetadata | an OAuth client: the scope a module's session gets |
com.fedfork.exec.source.file, .source.manifest | the source a module was built from, so anyone can rebuild it |
com.fedfork.exec.composition | modules wired together |
A module record names its session record and its source manifest by URI and CID. A pin (the wasm's sha256 plus the record's grant fields) is what a lockfile checks. See Publish a module.
Previous: Docs | Next: Run modules in a page