> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boat.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Patterns

> Three ways to run sandboxes for your users: always on per project, always on per user, or stop and resume around usage.

## Pattern 1: one always-on sandbox per project

This pattern is the simplest, and each project is fully isolated.

1. Make a sandbox for each user project with `--no-auto-stop`.
2. Clone or copy the user's code into it.
3. Run the user's dev server.
4. Expose the dev server.

<CodeGroup>
  ```bash CLI theme={null}
  boat ssh <id> "cd ~/project && npm run dev -- --host 0.0.0.0 --port 3000 &"
  boat ssh <id> "host 3000 --private"
  # prints https://<subdomain>-3000.on.boat.dev?_token=...
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"npm run dev -- --host 0.0.0.0 --port 3000 &","cwd":"project"}'

  curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host 3000 --private"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({
    sandboxId,
    command: "npm run dev -- --host 0.0.0.0 --port 3000 &", cwd: "project",
  });
  const hosted = await sandbox.command({ sandboxId, command: "host 3000 --private" });
  console.log(hosted.stdout); // https://<subdomain>-3000.on.boat.dev?_token=...
  ```

  ```python Python theme={null}
  sandbox.command(sandbox_id, CommandRequest(
      command="npm run dev -- --host 0.0.0.0 --port 3000 &",
      cwd="project",
  ))
  hosted = sandbox.command(sandbox_id, CommandRequest(command="host 3000 --private"))
  print(hosted.stdout)  # https://<subdomain>-3000.on.boat.dev?_token=...
  ```
</CodeGroup>

* Only the people you give the `_token` URL to can open it.
* The preview stays up while the sandbox runs.

| Size | Cost when it runs 24/7 |
| - | - |
| `default` | About \$26/month (\$0.00001 per second) |
| `large` | Two times the `default` cost |

Use this pattern when the project has real traffic, or when the user pays you enough to cover the cost.

## Pattern 2: one always-on sandbox per user

This pattern is like pattern 1. The difference is that one sandbox holds all the projects of one user.

* Each project has its own folder and its own port.
* You install a small daemon in the sandbox. It multiplexes the agent sessions.
* This works well for up to 5 to 10 fullstack projects per user.

The cost is about \$26/month for each user, not for each project. Be careful with isolation: the projects of one user share a machine.

## Pattern 3: stop and resume around usage

This pattern is the cheapest, and each sandbox is isolated. Most platforms end up with it. The sandbox runs only while the user works.

1. The user sends a message.
2. If the sandbox of the user is stopped, run `boat resume` on it. This takes a few seconds.
3. At the same time, start your daemon and the preview again.
4. Send the message to the agent.
5. When the agent finishes, wait a short countdown.
6. Run `boat stop`. The stop makes a snapshot of the file system and pauses billing.

| After a stop and resume | Comes back? |
| - | - |
| Files | Yes |
| Installed packages | Yes |
| Enabled systemd services | Yes |
| Processes you started by hand | No. Your daemon starts them again, or [run it as an always-on service](/long-running-tasks/always-on-services#run-an-always-on-service). |

See [Snapshots](/snapshots).

### When the user is done for good

A stop is a pause. A **delete** is the end.

* To delete, use `DELETE /sandboxes/{sandboxId}`, `boat delete`, or the `⋯` menu in the dashboard.
* A delete force-stops the sandbox. It permanently deletes the snapshots that only this sandbox uses.
* Boat keeps snapshot data that a fork, a resume or a named template still reads.
* You cannot undo a delete. You cannot resume the sandbox after it.

Connect delete to an explicit "delete my project" action. Never connect it to an idle timeout. If the user can come back, stop the sandbox instead.

See [Snapshots](/snapshots/download-and-delete#delete-a-sandbox).

### When a stop is refused

A stop saves the disk of the sandbox first. If that save fails, Boat **refuses the stop**. The sandbox keeps running, so the user does not lose work.

**You do not pay for that time.** The meter pauses by itself from the first failed attempt. A stuck sandbox never shows on your invoice, and you have nothing to claim back. See [When a stop is refused](/snapshots/stop#when-a-stop-is-refused).

What your backend sees:

1. Treat a refused stop as retryable, not as fatal.
2. The stop stays pending. Boat retries it until a save succeeds.
3. Then the sandbox stops by itself.
4. Boat also sends an email to the sandbox owner.

To stop the sandbox now, pass `force`.

* The sandbox stops with its last successful snapshot.
* Everything written after that snapshot is lost.
* You cannot undo it. Offer it only after a stop has already failed.
* Without `force`, the sandbox stops this way by itself after 3 days of failed saves. Boat sends an email to the owner before that.

<CodeGroup>
  ```bash CLI theme={null}
  boat stop bx_23456789 --force
  ```

  ```bash curl theme={null}
  curl -sS -X POST "https://boat.dev/api/v1/sandboxes/bx_23456789/stop" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"force":true}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.stop({ sandboxId: "bx_23456789", force: true });
  ```

  ```python Python theme={null}
  sandbox.stop(sandbox_id="bx_23456789", stop_request=StopRequest(force=True))
  ```
</CodeGroup>

### Cost

| | Cost |
| - | - |
| \$1 buys | About 27 hours of `default` machine time, or half that on `large` |
| Typical user | \$1 to \$5 per month |
| Power user | \$10 to \$20 per month |

To charge this cost to your users, read the meter of each sandbox for the billing period.

* Use `GET /sandboxes/{sandboxId}/usage`, or `boat usage <id>`.
* It works on running and stopped sandboxes.
* It counts exactly what Boat charged your balance for that sandbox.

See [Per-sandbox usage](/billing#per-sandbox-usage).

### Two improvements

* **No preview downtime.** Publish successful builds to a static host or a CDN. The sandbox is only for the agent and the dev preview. Production traffic never depends on a running sandbox.
* **No agent latency.** Run a copy of the agent without tools on your own server. It answers immediately and stalls while the sandbox resumes. For a working implementation, see [optibox](https://github.com/ariana-dot-dev/optibox).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.