BoxOS

Developer documentation

Build small, durable BoxOS apps.

Publish immutable pages, put state and authority in boxes, and compose asynchronous work with durable Tasks.

Quickstart

Download the dependency-free CLI, create an Ed25519 account, and publish an HTML page.

curl -fsSL https://boxos.org/boxos-cli.js -o boxos
chmod +x boxos
./boxos account create
./boxos page publish ./index.html

Each command writes one JSON value to stdout. The page command returns its immutable ID and public https://<page-id>.boxos.org/ URL.

Core model

  • Accounts are Ed25519 public keys. Private keys remain with clients.
  • Blobs are immutable text addressed by SHA-256.
  • Pages are immutable HTML blobs with short public IDs.
  • Boxes contain validated methods and their own public and private storage.

A box method produces one atomic commit plan for its local storage and Task declarations. One SQLite writer commits plans in order, while transfers and other non-local operations resolve as durable Tasks between turns.

Pages

Pages are ordinary HTML modules. Import the reference browser client from /client.js.

<!doctype html>
<button id="run">Run</button>
<script type="module">
  import { boxos } from "/client.js";

  document.querySelector("#run").onclick = async () => {
    const result = await boxos.invoke("BOX_ID", "increment", { amount: 1 }, { maxFuel: 10000 });
    console.log(result.value);
  };
</script>

Every page subdomain has a separate browser origin and an origin-scoped page account stored in IndexedDB.

Boxes

A box definition is JSON containing JavaScript method bodies. Methods receive ctx and input and run in a deliberately small, validated JavaScript subset.

{
  "methods": {
    "increment": "let n = ctx.storage.public.get(\"count\") || 0; n = n + input.amount; ctx.storage.public.set(\"count\", n); return n;"
  }
}

Add an optional 16-to-128-character nonce to create a box with independent storage while reusing identical methods. The nonce participates in the content hash but is not tied to an account and grants no ownership.

{
  "nonce": "550e8400-e29b-41d4-a716-446655440000",
  "methods": { "run": "return input;" }
}

Method context

ctx.account
ctx.clientId
ctx.time
ctx.self.methodName(input)
ctx.timeout(time, callback)
ctx.storage.public.get(key)
ctx.storage.public.set(key, value)
ctx.storage.private.get(key)
ctx.storage.private.set(key, value)
ctx.transfer(receiver, amount)
ctx.message(clientId, value)
ctx.invoke(boxId, method, input)
ctx.publish(kind, arguments)
ctx.request(request)

ctx.time is the turn's durable Unix time in milliseconds. ctx.self invokes another method synchronously in the same storage, fuel, and commit boundary. A self-call failure rolls back the outer turn.

ctx.timeout(time, callback) durably schedules a serializable callback for an absolute Unix time. It runs later as a fresh fuel-metered turn, receives the requested time, and can enter a method with ctx.self:

ctx.timeout(ctx.time + 100, function wake(scheduledTime) {
  return ctx.self.tick({ scheduledTime: scheduledTime });
});

Timeout callbacks cannot capture method locals. All methods remain public invocation entry points, including methods reached through ctx.self.

Box values are JSON-like: null, booleans, finite numbers, strings, arrays, and plain objects. Functions, Tasks, binary data, cycles, and undefined are not values.

Durable Tasks

ctx.invoke, ctx.publish, ctx.request, and ctx.transfer return runtime-owned Tasks. Returning a Task makes the caller wait for its complete chain; a transfer rejects if the originating account has insufficient fuel.

Each method and resumed continuation reserves a turn budget from the originating account. Methods are transpiled with deterministic fuel checks; runtime input, output, storage, message, continuation, and effect data are charged by size. Browser invocations can set an optional limit with { maxFuel: 10000 }.

return ctx.invoke(input.target, "read", input.query).then(
  function completed(result, saved) {
    ctx.storage.private.set(saved.key, result);
    return result;
  },
  { key: input.key }
);

Continuations execute later as fresh atomic turns. They cannot capture method locals; durable data must be supplied through the explicit callback context.

Tasks resemble Promises but are not native Promises. Box methods do not use async, await, or Promise.

CLI

./boxos dev create ./todo-app
./boxos box publish ./box.json
./boxos page publish ./index.html
./boxos invoke <box-id> increment '{"amount":1}'
./boxos blob publish ./data.txt
./boxos blob publish ./photo.webp
./boxos blob get <blob-id> --output ./photo.webp
./boxos storage get <box-id> count
./boxos startup
./boxos health

Single-file Bun apps

boxos dev create ./todo-app downloads the dependency-free developer library and creates a complete Todo example. Run it with bun app.js. The library turns named box functions into canonical validated definitions and generates a page around one ordinary browser init function.

import { box, page, publish } from "./boxos-dev.js";
const echo = box({ run: function run(ctx, input) { return input; } });
const app = page({ boxes: { echo }, init: async function init(app) {
  const result = await app.boxos.invoke(app.boxes.echo, "run", "hello");
  app.root.textContent = result.value;
} });
await publish(echo, app);

Box methods must be named function expressions with parameters (ctx, input). They are parsed locally, free references are rejected, and the server validates the extracted canonical bodies again. A box with dependencies uses (ctx, input, deps):

import { box, startup } from "./boxos-dev.js";
const secured = box({
  run: function run(ctx, input, deps) {
    return ctx.invoke(deps.grants, "check", input);
  }
}, { dependencies: { grants: startup("accounts.grants") } });

A page accepts exactly one named init function. It is serialized as ordinary browser JavaScript rather than parsed as trusted box code; visiting the page is the user’s trust decision. The function receives the root element, browser APIs, boxos, resolved boxes, pure page data, and optional account connection.

const app = page({
  account: { appName: "Example", permissions: ["manage example"] },
  init: async function init(app) {
    const signOut = app.document.createElement("button");
    signOut.textContent = "Change account";
    signOut.onclick = app.disconnectAccount;
    app.root.append(signOut);
  }
});

Generated pages link the current BoxOS default.css automatically, with app-specific styles loaded afterward; set defaultStyles: false to opt out. Publication reads ~/.boxos/account.json, honors BOXOS_KEY and BOXOS_URL, and emits one JSON result.

Publish images and videos

PNG, JPEG, WebP, GIF, and AVIF images are immutable public blobs up to 5 MiB and 8192 pixels on either axis. MP4 and WebM videos must be 10 seconds long (within 100 ms), no larger than 12 MiB, and at most 1280 pixels on either axis or 1280×720 total pixels. Portrait video is supported. Blob reads support HTTP byte ranges for playback and seeking.

const media = await boxos.publishFile(file);
preview.src = boxos.blobUrl(media.id);
const downloaded = await boxos.readBlob(media.id);

Box storage remains JSON-like and stores the returned blob ID, not the binary media bytes.

Link local boxes and blobs

Pages and boxes can reference a box definition by a path relative to the file containing the reference:

const counterBox = "{{BOXOS_BOX:./counter.box.json}}";
<img src="/v1/blobs/{{BOXOS_BLOB:./images/logo.png}}" alt="Logo">

Publishing resolves the complete graph, calculates its content IDs, and validates every linked box locally with the same parser used by the server. Only after the whole graph passes does the CLI publish boxes in dependency order and replace each marker with its immutable ID. Repeated paths are deduplicated and circular dependencies are rejected.

A parser rejection exits non-zero, reports the local box path, method, and source location, and publishes nothing. The server validates every box again as the security boundary.

Use --key, --url, BOXOS_KEY, and BOXOS_URL to override configuration. Run ./boxos --help for every command.

HTTP API

Public reads require no authentication. Mutations use canonical JSON and Ed25519 signatures.

GET  /health
GET  /v1/startup
GET  /v1/boxes/<box-id>
GET  /v1/boxes/<box-id>/storage/public?key=<key>
GET  /v1/blobs/<blob-id>
GET  /v1/pages/<page-id>
POST /v1/blobs        raw signed image or video bytes
POST /v1/boxes
POST /v1/invoke
POST /v1/operations
POST /v1/events

An invocation response is sent only after its returned durable Task settles. Exact signed-request replay is idempotent.