# Boxd Composable computer lifecycle - Package: @bijectionhq/boxd - Category: Sandboxes - Status: Preview - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/boxd/tree/21bc278b78c75f22af60d6e77624e41121af9e28 - Upstream: https://github.com/azin-tech/boxd-convex/tree/1061d4021dac2fbebd1717beb3c6124c7cbb6850 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/boxd ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/boxd.git cd boxd git checkout 21bc278b78c75f22af60d6e77624e41121af9e28 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @boxd-sh/convex; check the Bijection adaptation separately. 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 - 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 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 README # boxd for Convex Run [boxd](https://boxd.sh) 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://.boxd.sh`. - Scope every machine to a user or tenant of your app. - Get stable error codes to branch on (`NOT_FOUND`, `CONFLICT`, ...). ## Install ```sh npm install @boxd-sh/convex ``` Create an API key with the [boxd CLI](https://docs.boxd.sh) and set it on your deployment: ```sh 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: ```ts 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 ```ts // 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](#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 `ConvexError`s with `data: { code, message }`. Branch on the code with `isBoxdError`: ```ts 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 `exec` kills its command after `timeoutMs`: default 9 minutes, at most 9.5. For longer work, start it in the background (`nohup ... &`) and check on it with later commands. - **Output.** `exec` returns up to 2 million characters per stream. The execution row keeps the first 64,000. - **Files.** `readFile`, `readFileBytes` and `writeFile` move at most 8 MiB. Move bigger files with `exec`, for example with `curl`. - **New named routes.** `expose` with a `name` returns 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`](https://www.npmjs.com/package/@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`: ```ts 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 ```sh 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