Boxd
Composable computer lifecycle
git clone https://github.com/bijectionhq/boxd.git
cd boxd
git checkout 21bc278b78c75f22af60d6e77624e41121af9e28Bijection repository access is required. Request access.
Description
Composable computer lifecycle
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/boxd. Based on azin-tech/boxd-convex. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
Compute runs with the external sandbox provider. Bijection connects application state and operations to that provider.
From the upstream publisher
Convex component for boxd composable computers
Explore the demo ↗Demo video
Upstream overview
From the upstream package documentation.
The boxd Convex component lets you create, fork, and control full Linux microVMs from Convex actions, with machine state and command output stored in reactive Convex tables. It wraps the boxd API to handle authentication token management, owner-scoped access control, and idempotent lifecycle operations including pause, hibernate, wake, stop, and destroy. Queries over machine rows and execution history re-run automatically when state changes, so your UI stays current without polling.
Use cases
From the upstream package documentation.
- Building AI coding agents that need isolated Linux environments to run arbitrary code, install packages, and persist state between steps
- Spinning up per-user or per-tenant sandboxed compute environments where each user gets their own microVM scoped to their identity
- Running long-lived background processes on a machine and checking output later with subsequent exec calls, working around Convex's 10-minute action limit
- Forking a known-good machine state as a starting point for each task, similar to branching a git repo, to avoid cold setup time
- Exposing a port from a microVM to a stable public HTTPS URL so a Convex-backed app can serve web traffic directly from a machine
How it works
From the upstream package documentation.
Install the component and register it in convex/convex.config.ts using app.use(boxd, { env: { BOXD_API_KEY: app.env.BOXD_API_KEY } }). The API key is bound into the component's own environment so it never appears as a function argument. The component exchanges the key for a session token once, stores it in its own table, and reuses it across actions to avoid hitting boxd's rate limit on token exchanges per source IP.
Instantiate Boxd with new Boxd(components.boxd) in your convex functions file. Methods like create, fork, exec, pause, hibernate, and destroy run in Convex actions and call boxd through the @boxd-sh/sdk web entry. Read methods like list, get, listExecutions, and getExecution run in queries and return reactive data directly from the component's tables.
Every machine is addressed with both a machineId and an ownerId. Passing a mismatched owner returns NOT_FOUND, the same as a nonexistent machine, which enforces tenant isolation without extra guards. Errors are typed ConvexError values with a code field, and the exported isBoxdError helper lets you branch on specific codes like NOT_FOUND, CONFLICT, or TIMEOUT.
Machine rows update immediately when a component call changes state, but boxd can also change machines out-of-band, for example via autoSuspendSeconds. Call refresh on a timer to sync those external state changes back into the reactive table.
Upstream package FAQ
What is the boxd Convex component and what does it do?
The boxd Convex component (@boxd-sh/convex) integrates the boxd microVM platform with a Convex backend. It lets you create, fork, pause, hibernate, and destroy Linux microVMs from Convex actions, and stores machine state and command execution history in reactive Convex tables so your frontend can subscribe to updates with ordinary queries.
How does the boxd component handle authentication and API keys?
You create a boxd API key with the boxd CLI and set it as a Convex environment variable using `npx convex env set BOXD_API_KEY bxd_...`. The key is bound into the component's own environment in `convex.config.ts` and never passed as a function argument. The component exchanges the key for a session token once, caches it in its own table, and reuses it until expiry.
How do I scope boxd machines to individual users in my Convex app?
Pass an `ownerId` derived from Convex auth on every boxd call. The component enforces that a machine created with a given `ownerId` is only accessible when that same `ownerId` is provided. Reads for other owners return null or empty arrays, and writes fail with a NOT_FOUND error, matching the behavior for machines that do not exist.
What are the execution time and output limits for boxd commands run from Convex?
Convex enforces a 10-minute action limit, so `boxd.exec` kills its command after a `timeoutMs` that defaults to 9 minutes and is capped at 9.5 minutes. Each stream returns up to 2 million characters, and the execution row in Convex stores the first 64,000 characters. For longer-running work, the README recommends starting commands with nohup in the background and polling with subsequent exec calls.
How do I test my Convex app that uses the boxd component without making real network calls?
The boxd component ships a test helper at `@boxd-sh/convex/test`. Register it with `convex-test` using `boxdTest.register(t)`, then mock `@boxd-sh/sdk/web` with `vi.mock` to intercept network calls. The component's own test suite in `src/component/setup.test.ts` demonstrates this pattern.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Boxd with Bijection.
Read: https://bijection.com/components/boxd/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from azin-tech/boxd-convex, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
boxd for Convex
Run boxd machines from your Convex backend. Each machine is a
full Linux microVM with its own disk, memory and HTTPS URL. boxd cold-boots a
machine in under 10 ms and forks one like a git branch in under 200 ms, and a
machine sleeps and wakes with its memory intact. Through this component,
create and fork return in under a second, because they also wait until the
new machine accepts a command.
The component keeps a reactive row for every machine and every command it runs, so your UI subscribes to machine state and command output with a plain Convex query.
- Create, fork, pause, resume, hibernate, wake, stop, start and destroy machines.
- Run commands and read and write files.
- Put a port on the public internet:
https://<machine>.boxd.sh. - Scope every machine to a user or tenant of your app.
- Get stable error codes to branch on (
NOT_FOUND,CONFLICT, ...).
Install
npm install @boxd-sh/convex
Create an API key with the boxd CLI and set it on your deployment:
boxd auth keys create convex
npx convex env set BOXD_API_KEY bxd_...
A key is fenced to one boxd org. Machines are created in, and billed to, that org.
In a brand-new Convex project, run npx convex dev once first so there is a
deployment to set the key on. Its first push fails until the key is set, since
the component requires it.
Install the component in convex/convex.config.ts, and bind the key into it:
import { defineApp } from "convex/server";
import { v } from "convex/values";
import boxd from "@boxd-sh/convex/convex.config.js";
const app = defineApp({
env: { BOXD_API_KEY: v.string() },
});
app.use(boxd, { env: { BOXD_API_KEY: app.env.BOXD_API_KEY } });
export default app;
The key goes into the component's own environment, so it never travels as a function argument.
Use it
// convex/machines.ts
import { Boxd } from "@boxd-sh/convex";
import { v } from "convex/values";
import { components } from "./_generated/api";
import {
action,
query,
type ActionCtx,
type QueryCtx,
} from "./_generated/server";
const boxd = new Boxd(components.boxd);
// The component can't see your users, so derive the owner from your auth.
// Never take it from a client argument.
async function ownerId(ctx: QueryCtx | ActionCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
return identity.subject;
}
export const createMachine = action({
args: {},
handler: async (ctx) => {
return await boxd.create(ctx, {
ownerId: await ownerId(ctx),
vcpu: 1,
autoSuspendSeconds: 300,
});
},
});
export const run = action({
args: { machineId: v.string(), command: v.string() },
handler: async (ctx, { machineId, command }) => {
return await boxd.exec(ctx, {
ownerId: await ownerId(ctx),
machineId,
command,
});
},
});
// Reactive: re-runs whenever one of this user's machine rows changes.
export const myMachines = query({
args: {},
handler: async (ctx) => boxd.list(ctx, { ownerId: await ownerId(ctx) }),
});
// Reactive: every command's status and output, newest first.
export const history = query({
args: { machineId: v.string() },
handler: async (ctx, { machineId }) =>
boxd.listExecutions(ctx, { machineId, ownerId: await ownerId(ctx) }),
});
example/convex/example.ts wraps every method this way.
API
Every method takes the Convex ctx first. Reads run in queries. Everything that
calls boxd runs in actions. Every call that addresses a machine takes the
ownerId it was created with: leave it out and a machine created with an owner
is NOT_FOUND (see Owners).
| Method | What it does |
|---|---|
create(ctx, { ownerId, ... }) | Create a machine. Waits until it accepts a command, unless waitUntilReady: false |
fork(ctx, { machineId, ownerId }) | Copy a machine, disk and memory, into a new one with the same owner |
exec(ctx, { machineId, ownerId, command }) | Run a command, wait for it to exit, record it. command is a string or an argv |
readFile / readFileBytes | Read a UTF-8 file as text, or any file byte for byte |
writeFile(ctx, { machineId, ownerId, path, content }) | Write text or bytes. The parent directory must exist |
listDir(ctx, { machineId, ownerId, path }) | List a directory |
expose(ctx, { machineId, ownerId, port, name? }) | Route public HTTPS to a port. Returns the URL |
pause / resume | Freeze in RAM, and thaw |
hibernate / wake | Save memory to disk and free the host, and restore |
stop / start | Shut down and cold boot. The disk is kept |
refresh(ctx, { machineId, ownerId }) | Re-read the machine from boxd into its row |
destroy(ctx, { machineId, ownerId }) | Destroy the machine. Its row and history stay, with status destroyed |
get / list | Machine rows, reactive |
listExecutions / getExecution | Command history, reactive |
create takes name, image, env, vcpu or memory (size classes 1, 2 or
4 vCPU with 4, 8 or 16 GiB), autoSuspendSeconds, autoDestroySeconds and
isolated. The component never stores env.
Owners
Pass ownerId on every call. A machine created with an ownerId is reachable
only with that same ownerId, and one created without one only without one. For
any other owner, reads return null or [], and actions fail with NOT_FOUND,
the same as for a machine that doesn't exist. A fork gets its source's owner.
Keeping rows current
Every row updates the moment a call through the component changes it, and
queries over them re-run on their own. boxd can also change a machine without a
call, for example auto-suspend after autoSuspendSeconds or auto-destroy after
autoDestroySeconds. Call refresh to pick that up, for instance on a timer
while a machine is on screen.
Sleeping machines
A paused or hibernated machine wakes by itself when it gets a command or an
HTTPS request, so exec works without a resume first. Every transition is
idempotent: resume on a running machine succeeds.
Errors
Errors are ConvexErrors with data: { code, message }. Branch on the code
with isBoxdError:
import { isBoxdError } from "@boxd-sh/convex";
try {
await boxd.refresh(ctx, { machineId, ownerId });
} catch (error) {
if (isBoxdError(error, "NOT_FOUND")) return null;
throw error;
}
| Code | Meaning |
|---|---|
INVALID_ARGUMENT | An argument is wrong. Nothing was sent to boxd |
NOT_FOUND | No such machine for this owner, or no such file |
CONFLICT | The machine's state doesn't allow this, e.g. pause when stopped |
UNAUTHENTICATED | BOXD_API_KEY is missing, invalid, expired or revoked |
PERMISSION_DENIED | The key's org may not do this |
RATE_LIMITED | A quota or rate limit, e.g. the org's machine quota |
TIMEOUT | A command ran past its timeoutMs, or a machine wasn't ready in time |
UNAVAILABLE | boxd could not be reached |
BOXD_ERROR | Anything else |
A failed lifecycle call also stores its message in the machine's lastError.
The next successful call clears it.
When create or fork makes a machine that then fails to become ready, the
error's data.machineId names it. The machine exists, and bills, until you
destroy it, so act on that id instead of creating another machine.
Limits
- 10 minute actions. Convex stops an action after 10 minutes, so
execkills its command aftertimeoutMs: default 9 minutes, at most 9.5. For longer work, start it in the background (nohup ... &) and check on it with later commands. - Output.
execreturns up to 2 million characters per stream. The execution row keeps the first 64,000. - Files.
readFile,readFileBytesandwriteFilemove at most 8 MiB. Move bigger files withexec, for example withcurl. - New named routes.
exposewith anamereturns at once, but the new URL answers HTTPS only after boxd issues its certificate, typically 15 to 20 seconds later. The default URL answers at once.
How it works
The component calls boxd through the
@boxd-sh/sdk web entry, which
runs in Convex's default runtime. It exchanges the API key for a session token
once, keeps the token in its own table, and reuses it until it nears expiry.
boxd rate-limits that exchange per source IP, and Convex deployments share their
egress IPs, so each action must not exchange the key again. If boxd revokes the
token early, the component exchanges the key again and retries the call once.
Testing your app
Register the component with convex-test:
import { convexTest } from "convex-test";
import boxdTest from "@boxd-sh/convex/test";
import schema from "./schema";
const modules = import.meta.glob("./**/*.ts");
const t = convexTest(schema, modules);
boxdTest.register(t);
To keep tests off the network, mock @boxd-sh/sdk/web with vi.mock. The
component's own tests do this: see src/component/setup.test.ts.
Developing this component
npm install
npm run build:codegen # codegen and build dist/
npm run check # build, typecheck, lint, format check, unit tests
npm run test:live # the example app against real boxd machines
test:live runs the example app on an anonymous local Convex backend, with no
Convex account and nothing uploaded, against real boxd machines. It needs
BOXD_API_KEY in the environment or in .env.e2e.local, and destroys every
machine it creates.
demo/ is the public demo: a small React page (demo/src) that boots a
machine, runs commands and forks it, all through the component, with its own
Convex backend in demo/convex. That backend adds the parts a public page needs
on top of the component: anonymous sign-in, an optional password
(DEMO_PASSWORD), per-visitor machine caps, rate limits, and a cron that
destroys each machine after ten minutes. Demo machines are isolated, so
visitor commands can't reach boxd or other machines. Run it with
npx convex dev and npm run dev:demo, or build it with npm run build:demo.
example/convex is a reference app that wraps the whole API with no limits. It
backs the unit and live tests and is never deployed alongside the demo, since
its functions are open to any signed-in user.
License
MIT