Ship a product that gives every user a machine

Create and manage a machine per user through the SDK, without operating a fleet by hand.

Michiel VoortmanMichiel Voortman7 min readUse cases
A creature behind a counter hands numbered keys to a queue while a wall of small identical lit rooms runs away into the distance behind it.
On this page · 7 min

Your product hands each customer an agent, a workspace, or a running app instance. It has to keep files between sessions, remember what it did on Tuesday, run whatever the customer asks it to run, and answer at a URL. So you open an empty file and start designing a sandbox, and somewhere around hour three you realise you are designing an operating system.

The usual shortcut is to run every customer inside one shared process behind a wall of checks. It works on day one. Then you spend the rest of the product's life proving that customer A cannot see customer B, and every new feature asks the question again, and the answer is only ever as good as whoever thought about it last.

Give each customer a machine and the question changes shape. Every boxd machine is hardware isolated, so one tenant's code can never read another tenant's memory or disk. That is a property of the platform rather than of your request handler, and it does not quietly regress when someone ships on a Friday.

It is not free, though. You now have a fleet, and a fleet has names, versions, and a lifecycle. Three steps get you from an empty account to a multi-tenant product, and this article is honest at the end about which parts stay yours.

Build the tenant image once

Your agent harness is yours. Install it on a machine the way you would install it on any Linux box, then save the result as a snapshot.

Terminal
boxd machine new harness-builder
boxd connect harness-builder            # install your harness, or run /boxd-setup-hermes
boxd snapshots save harness-builder agent-harness-v1

Hermes is one example of a harness, and /boxd-setup-hermes installs it for you. See its setup.

That snapshot is your tenant image. Do it by hand, once. Writing an installer that runs correctly on every signup is a worse use of a week than typing the commands yourself while you watch them work, and every tenant machine in the fleet starts from this one prepared disk.

One machine per tenant

Your backend creates a machine when a customer signs up. Name it after the tenant, build it from the snapshot, set isolated.

TypeScript
import { Boxd } from "@boxd-sh/sdk";

const boxd = new Boxd();                    // reads BOXD_API_KEY

async function createTenant(tenant: string, secrets: Record<string, string>) {
  const machine = await boxd.machines.create({
    name: `tenant-${tenant}`,
    fromSnapshot: "agent-harness-v1",
    isolated: true,
  });
  await boxd.machines.waitUntilReady(machine.id);

  // Isolated machines receive no account-level env vars or secrets,
  // so the tenant's configuration goes in through code.
  const env = Object.entries(secrets).map(([k, v]) => `${k}=${v}`).join("\n");
  await boxd.machines.files.upload(machine.id, "/home/boxd/agent/.env", env);
  await boxd.machines.exec(machine.id, { command: "sudo systemctl restart agent" });

  return machine.access.url;
}

console.log(await createTenant("acme", { OPENAI_API_KEY: "sk-..." }));
// https://tenant-acme.boxd.sh

That is the entire provisioning path. The Python SDK mirrors it with from_snapshot and wait_until_ready, and the whole function sits comfortably inside your existing signup handler, next to the row you insert in your users table.

Two lines deserve a second look. The name is the join between your user table and your fleet, so pick a scheme now and never improvise on it later. And the upload is how per-tenant secrets arrive: isolated machines receive no account-level env vars or secrets, so configuration goes in through code at create time. Account-level env vars and secrets reach your own machines and deliberately never reach a tenant sandbox, which is inconvenient exactly once and correct forever after.

The exec at the end restarts the agent so it picks up the file you just wrote. The function returns machine.access.url, which is the tenant's HTTPS endpoint, live as soon as the machine is ready.

Shipping a new version

Snapshots are named and versioned, so agent-harness-v1 means the same environment for every tenant sitting on it. Rolling out v2 is a new snapshot rather than a migration.

New tenants get v2 immediately. Existing tenants get it on their next recreate, and deciding when that happens is your backend's job, tenant by tenant. There is no fleet-wide upgrade button here, which cuts both ways: nobody can roll your customers forward without you, and nobody will do it for you either.

A fleet that is mostly asleep

Per-tenant VMs sound expensive right up until you look at what an idle one costs. Most tenants are idle most of the time, and each machine winds itself down without you tracking who is active.

There are two resting states, and they bill differently.

Suspended freezes the machine with its memory held in RAM. You stop paying for vCPU and keep paying for RAM and disk. It resumes in sub-millisecond time, so a tenant clicking around a dashboard never waits for it.

Hibernated writes that memory to disk and releases the RAM, so you pay for disk alone. Waking takes about 85ms, which is under the latency a person notices on a page load, and the caller cannot tell the machine was ever off.

Machines hibernate on their own after four hours without network traffic. Auto-suspend is off by default and worth turning on for a shorter window, so a tenant who steps away for lunch stops costing vCPU within the minute and is still instant when they come back.

TypeScript
await boxd.machines.setAutoSuspendTimeout(machine.id, 60);

A tenant who leaves on Friday and returns on Monday costs you disk over the weekend, and their machine answers Monday's first request as if it never slept.

One case needs the opposite setting. Both timers watch network traffic rather than CPU, so a tenant machine chewing through a background job with nothing arriving at its URL looks idle and gets parked mid-job. Turn hibernation off on machines that are meant to keep working while nobody is looking:

TypeScript
await boxd.machines.setAutoHibernateTimeout(machine.id, 0);

See Suspend, resume, and hibernate.

Your brand on the URL

Tenant machines can live under your own domain. Delegate one wildcard subdomain to your org and every machine gets a name under it, current and future.

Terminal
boxd manage domain set vms.mysaas.com

After that, tenant-42 answers at https://tenant-42.vms.mysaas.com with TLS issued automatically. For a single flagship app you can also bind one specific domain to one machine. See Custom domains.

How far apart tenants sit

Hardware isolation is always there, underneath everything else. The settings below control the network on top of it.

SettingWho reaches whom
isolatedThe sandbox reaches none of your other machines, ever, beyond networks you grant it explicitly
Network labels, no flagMachines inside one tenant reach each other, and tenants stay apart
No labels, no flagMachines share your account's private network, which suits a fleet that works together

isolated is the strictest of the three and the right default for a tenant sandbox. It is also strict about its own kind: isolated machines never reach each other, even on a shared network. So a tenant that needs several cooperating machines uses network labels without the flag. See Sandboxes.

What is still yours

boxd gives you a machine, a URL, a snapshot, and a lifecycle. That is the infrastructure, not the product. Five things stay firmly on your side of the line, and it is better to know that now than to discover it in month two.

  • The harness. boxd runs Linux. What you install on it is your product, and nobody else can write it for you.
  • The name scheme. Your user table maps to machine names. That mapping is yours to keep consistent, including when a customer renames their org.
  • The secrets. Each tenant's configuration goes in through code at create time, from wherever you keep secrets today.
  • The lifecycle calls. Your backend decides when a tenant gets a machine, when it gets recreated on a new snapshot version, and when it goes away.
  • Your pricing. The fleet has a cost to you. What you charge your customers is a separate decision, and a more interesting one.

Plan around the defaults. Each machine gets 2 vCPU, 8 GiB RAM, and a 100 GB copy-on-write disk, with bigger shapes available on request. Accounts start with a cap of 50 concurrent machines, which a one-machine-per-tenant product will meet sooner than most workloads do. Raises for real workloads are usually same-day, so email contact@boxd.sh with what you are building before you need it, not after. See Resources and limits.

Do the first two steps by hand

Before writing any code, create a machine, install your harness on it, and snapshot it. Then call create twice with two different tenant names and open both URLs.

If they both answer, you are looking at the entire architecture. Everything after that is your product, which is where the interesting work should have been all along.

Michiel VoortmanMichiel Voortman
PostShare
Published
Aug 21, 2026
Reading time
7 min
Words
1,386
Topic
Use cases

Read next

Field notes

Subscribe for release notes and architecture write-ups

No spam, ever. Unsubscribe anytime.

Your inbox