# Volume

Volume provides persistent storage for Sandbox0. It is a storage unit independent of the Sandbox lifecycle, allowing data sharing and reuse across multiple Sandboxes.

## Why Volumes?

The default Sandbox writable filesystem is checkpointed across pause/resume for the same Sandbox identity. Sandbox rootfs snapshots and forks can preserve or branch that rootfs state, but they still operate at the sandbox rootfs boundary. Volumes solve the cases where data must be mounted by multiple Sandboxes or accessed directly through storage APIs:

- **Data Persistence**: Store data that needs long-term retention, such as databases, model files, and user uploads
- **Cross-Sandbox Sharing**: Mount the same Volume to multiple Sandboxes for data sharing
- **Fast Snapshots**: Create point-in-time snapshots in seconds for backup and versioning
- **Fast Forking**: Create independent child Volumes with Copy-on-Write isolation
- **Quick Recovery**: Restore from snapshots quickly, ideal for rollbacks and environment cloning

| Storage surface | Survives pause/resume | Survives sandbox delete or `hard_ttl` | Shareable | Snapshots and forks | Direct file API |
|-----------------|-----------------------|----------------------------------------|-----------|---------------------|-----------------|
| Sandbox root filesystem | Yes, after checkpointed pause | Only through named rootfs snapshots or forks | No | Yes, through [Snapshot And Restore](/docs/sandbox/snapshot-restore) | Through the Sandbox file API while the Sandbox exists |
| Sandbox Volume | Yes | Yes, until the Volume is deleted | Yes | Backend-dependent; `s0fs` only | Backend-dependent; `s0fs` always, `s3` only through an active mount owner |

## Volume and Sandbox Relationship

```mermaid
flowchart LR
    vol["Volume"]
    sb["Sandbox mount<br/>RWO or ROX"]
    api["Direct volume file API"]

    vol --> sb
    vol --> api
```

## Access Modes

Volumes support three access modes:

| Mode | Full Name | Description | Typical Use Cases |
|------|-----------|-------------|-------------------|
| `RWO` | Read-Write Once | One writable owner at a time | Agent workspaces, databases, exclusive write-heavy state |
| `ROX` | Read-Only Cross | Multi-sandbox read-only distribution | Shared model files, static assets, reference datasets |
| `RWX` | Read-Write Cross | Shared read-write volume mode for direct API workflows | Control-plane style file workflows and shared storage patterns that do not rely on sandbox mounts |

<Callout variant="info">
The default access mode is `RWO`. Sandbox mounts are declared by the template and bound at claim time. `RWO` mounts use node-local write-ahead logging for low-latency small-file workloads, `ROX` is for read-only sharing, and `RWX` is not accepted for sandbox mounts in the current node-local mount path.
</Callout>

## Volume Backends

The default backend is `s0fs`, Sandbox0's durable POSIX-oriented volume store backed by S3-compatible object storage. Sandbox0 also supports an `s3` backend for mounting an existing S3-compatible bucket prefix into a Sandbox.

Backend choice controls which operations are available. `s0fs` supports snapshots, restore, forks, and direct file APIs without a running Sandbox. `s3` is a mount-oriented object prefix projection and does not support snapshots, restore, or forks.

For the full compatibility matrix and S3 backend examples, see [Volume Backends](/docs/sandbox/volume/backends).

## Correctness Model

For mounted `RWO` volumes, Sandbox0 keeps one active writable owner at a time.

- when a sandbox mounts an `RWO` volume, the node-local mount owner becomes authoritative for reads and writes
- direct volume file API requests are routed to that mounted owner instead of opening a second writable mount
- for `s0fs` volumes that are not mounted into a sandbox, `storage-proxy` serves direct file API requests itself

This keeps the mounted filesystem view and the direct volume API view consistent in both directions. For `s3` backend Volumes, direct file API requests require an active ctld mount owner; otherwise use S3 APIs directly or mount the Volume into a Sandbox first.

## Application-Layer Encryption

Sandbox0 encrypts persisted S0FS volume data at the application layer before it is written to object storage.

Manifest objects are encrypted as full blobs. Segment objects are encrypted in independently authenticated chunks, so cold file reads can still fetch only the ciphertext chunks needed for the requested byte range. Node-local cache files, including the S0FS WAL, head state, and snapshot state, are also encrypted before they are written to disk.

<Callout variant="info">
This is service-side application-layer encryption. `storage-proxy` and `ctld` hold the volume encryption key so they can serve normal POSIX filesystem reads and writes. It is not end-to-end encryption where Sandbox0 services cannot decrypt volume data.
</Callout>

For self-hosted deployments, see [Self-hosted Configuration](/docs/sandbox/self-hosted/configuration) for the storage-proxy encryption setting.

## Direct File Operations

For `s0fs` Volumes, you can operate on files directly by Volume ID without mounting the Volume into a Sandbox first.

This is useful for SDK workflows, developer tooling, and small control-plane style file tasks where starting or reusing a Sandbox would only add latency.

<Callout variant="info">
Direct volume file APIs still go through the normal gateway chain and team-scoped auth. When an `s0fs` volume is already mounted into a sandbox, the API request is served by that mounted owner. When it is not mounted, `storage-proxy` lazily attaches the volume on demand and reclaims idle direct mounts later. `s3` backend Volumes require an active mounted owner for direct file API requests.
</Callout>

The direct file API surface mirrors the Sandbox file API:

| Operation | Endpoint |
|-----------|----------|
| Read / Write / Delete | `GET`, `POST`, `DELETE /api/v1/sandboxvolumes/{'{id}'}/files?path=...` |
| Stat | `GET /api/v1/sandboxvolumes/{'{id}'}/files/stat?path=...` |
| List directory | `GET /api/v1/sandboxvolumes/{'{id}'}/files/list?path=...` |
| Move / Rename | `POST /api/v1/sandboxvolumes/{'{id}'}/files/move` |
| Watch | `GET /api/v1/sandboxvolumes/{'{id}'}/files/watch` |

Paths are always resolved relative to the root of the Volume namespace.

For SDK, CLI, and direct HTTP file workflows, see the dedicated [Volume HTTP](./http) page.

---

## Create Volume

Create a new persistent volume with an access mode.

<Endpoint method="POST">
/api/v1/sandboxvolumes
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `volume, err := client.CreateVolume(ctx, apispec.CreateSandboxVolumeRequest{
    AccessMode: apispec.NewOptVolumeAccessMode(apispec.VolumeAccessModeRWO),
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Volume ID: %s\\n", volume.ID)`
    },
    {
      label: "Python",
      language: "python",
      code: `from sandbox0.apispec.models.create_sandbox_volume_request import CreateSandboxVolumeRequest
from sandbox0.apispec.models.volume_access_mode import VolumeAccessMode
    
volume = client.volumes.create(CreateSandboxVolumeRequest(
    access_mode=VolumeAccessMode.RWO,
))
print(f"Volume ID: {volume.id}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `import { models } from "sandbox0";

const volume = await client.volumes.create({
    accessMode: models.VolumeAccessMode.Rwo,
});
console.log("Volume ID:", volume.id);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `# Create a volume
s0 volume create --access-mode RWO

# For automation, prefer machine-readable output
s0 volume create --access-mode RWO -o json`
    }
  ]}
/>

---

## Get Volume Details

Retrieve a specific volume by ID.

<Endpoint method="GET">
/api/v1/sandboxvolumes/{'{id}'}
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `vol, err := client.GetVolume(ctx, volume.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Volume: %s (backend: %s)\\n", vol.ID, vol.Backend)`
    },
    {
      label: "Python",
      language: "python",
      code: `vol = client.volumes.get(volume.id)
print(f"Volume: {vol.id} (backend: {vol.backend})")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const vol = await client.volumes.get(volume.id);
console.log("Volume:", vol.id, "backend:", vol.backend);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume get vol_abc123xyz`
    }
  ]}
/>

---

## List Volumes

List all volumes in the current team.

<Endpoint method="GET">
/api/v1/sandboxvolumes
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `volumes, err := client.ListVolume(ctx)
if err != nil {
    log.Fatal(err)
}
for _, v := range volumes {
    fmt.Printf("- %s (%s)\\n", v.ID, v.Backend)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `volumes = client.volumes.list()
for vol in volumes:
    print(f"- {vol.id} ({vol.backend})")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const volumes = await client.volumes.list();
for (const vol of volumes) {
    console.log(vol.id, vol.backend);
}`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume list`
    }
  ]}
/>

## Delete Volume

Delete a volume when it is no longer needed.

<Endpoint method="DELETE">
/api/v1/sandboxvolumes/{'{id}'}
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `_, err = client.DeleteVolume(ctx, volume.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Println("Volume deleted")`
    },
    {
      label: "Python",
      language: "python",
      code: `client.volumes.delete(volume.id)
print("Volume deleted")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `await client.volumes.delete(volume.id);
console.log("Volume deleted");`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume delete vol_abc123xyz`
    }
  ]}
/>

## Next Steps

<CardGroup>
  <Card title="Mounts" href="/docs/sandbox/volume/mounts" cta="Continue">
    Mount volumes into sandbox templates and claims with correct access modes.
  </Card>

  <Card title="HTTP" href="/docs/sandbox/volume/http" cta="Continue">
    Use direct volume file APIs outside a running sandbox mount.
  </Card>

  <Card title="Backends" href="/docs/sandbox/volume/backends" cta="Continue">
    Choose between `s0fs` and S3-compatible backend behavior.
  </Card>
</CardGroup>
