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

# Snapshots and policies

> Point-in-time snapshots, restoring from one, and the schedules that take them for you.

## Snapshots

A snapshot is a point-in-time copy of one volume:

<Tabs>
  <Tab title="Console">
    Go to **Storage → Snapshots** and choose **Create Snapshot**, then pick the
    **Volume** and give it a **Name**. The volume's own page has a **Create
    Snapshot** button that opens the same form with that volume already
    selected; it is disabled unless the volume is `available` or `in_use`.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/snapshots
    { "volume_id": "5f8d2c1a-...", "name": "pre-upgrade" }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage snapshot create --volume-id <volume-id> --name pre-upgrade
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    snap, err := storage.New(cfg).CreateSnapshot(ctx, &storage.SnapshotCreateRequest{
        VolumeID: volumeID,
        Name:     "pre-upgrade",
    })
    ```
  </Tab>
</Tabs>

<Steps>
  <Step title="Take it">
    Allowed against a volume in `available` **or** `in_use` — you do not have
    to detach to snapshot. The snapshot is created asynchronously (`202`,
    status `creating`).
  </Step>

  <Step title="Wait for available">
    Poll `GET /v1/snapshots/{snapshot_id}`. `available` means the snapshot is
    complete and can be restored from; `error` puts the reason in
    `error_message`.
  </Step>
</Steps>

A snapshot name is unique **per volume**, so `nightly` on two different volumes
is fine. `size_gb` is the volume's size frozen at the moment the snapshot was
taken — the volume may have been extended since, so do not read it as the
volume's current size, and do not read it as the space the snapshot occupies.

<Warning>
  Snapshotting an attached volume captures the device as it is at that instant,
  including anything the guest has buffered but not yet flushed. For a database
  or anything else with in-memory state, quiesce or flush inside the guest
  before taking the snapshot if you need it to be application-consistent.
</Warning>

### Restoring from a snapshot

Restoring means creating a **new** volume from the snapshot. There is no
in-place rollback:

<Tabs>
  <Tab title="Console">
    On **Create Volume**, switch **Source** to **From snapshot** and choose the
    **Snapshot**. **Size (GB)** must be at least the snapshot's size, and the
    **Tier** is yours to pick — a restore is not tied to the tier the source
    volume was on.

    **Storage → Snapshots** also has a **Create volume from snapshot** row
    action that opens the same form with the snapshot filled in.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/volumes
    {
      "name": "app-data-restored",
      "volume_type": "ssd",
      "size_gb": 200,
      "source_snapshot_id": "7b1e9c4d-..."
    }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage volume create \
      --name restored-01 --volume-type ssd --size-gb 100 \
      --source-snapshot-id <snapshot-id>
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    vol, err := storage.New(cfg).CreateVolume(ctx, &storage.VolumeCreateRequest{
        Name:             "restored-01",
        VolumeType:       "ssd",
        SizeGB:           100,
        SourceSnapshotID: basaltic.String(snapshotID),
    })
    ```

    A restore is a new volume, so the tier is yours to pick — it need not match
    the volume the snapshot came from.
  </Tab>
</Tabs>

The snapshot has to be `available`, and `size_gb` must be at least the
snapshot's frozen size — you can restore onto a larger volume, never a smaller
one. The restored volume records `source_snapshot_id`, which is also what stops
retention from reaping a snapshot something still depends on.

## Snapshot policies

A policy is a schedule attached to one volume: take a snapshot every
`interval_minutes`, then keep at most `retention_count` of the snapshots that
policy created.

<Tabs>
  <Tab title="Console">
    A schedule belongs to one volume, so it lives on that volume rather than in
    a list of its own. Open the volume, go to its **Settings** tab, and fill in
    the **Snapshot schedule** card: **Name**, **Every (minutes)**, **Keep**, and
    optionally **Also delete after (days)**. Choose **Attach schedule**.

    Once a schedule exists the card gains a switch that pauses it, and its
    buttons become **Save** and **Remove schedule**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/snapshot-policies
    {
      "volume_id": "5f8d2c1a-...",
      "name": "nightly",
      "interval_minutes": 1440,
      "retention_count": 7,
      "retention_days": 30
    }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage snapshot-policy create \
      --volume-id <volume-id> --name nightly \
      --interval-minutes 1440 --retention-count 7
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    pol, err := storage.New(cfg).CreateSnapshotPolicy(ctx, &storage.SnapshotPolicyCreateRequest{
        VolumeID:        volumeID,
        Name:            "nightly",
        IntervalMinutes: 1440,
        RetentionCount:  7,
    })
    ```
  </Tab>
</Tabs>

<ResponseField name="volume_id" type="one policy per volume" required>
  Attaching a second policy to the same volume is a `409`. Update the existing
  one instead.
</ResponseField>

<ResponseField name="interval_minutes" type="1–43200" required>
  A **minimum gap**, not an exact cadence. A snapshot lands at or after the
  interval and never before, and can land a minute or two late when the
  scheduling pass is busy. The floor is one minute because that is how often
  the pass evaluates schedules; the ceiling is 30 days.
</ResponseField>

<ResponseField name="retention_count" type="1–256" required>
  How many of this policy's snapshots to keep. When a run takes the count past
  this, the oldest go first.
</ResponseField>

<ResponseField name="retention_days" type="0–3650, default 0">
  An optional age bound applied **on top of** the count: a snapshot outside
  either window is reaped. `0` means no age bound.
</ResponseField>

<Note>
  The first snapshot lands one interval from now. Attaching a schedule is not
  itself a request for a snapshot — use `POST /v1/snapshots` if you want one
  immediately.
</Note>

Scheduled snapshots are named `<policy>-<UTC timestamp>`, for example
`nightly-20260115T000000Z`, and they inherit the policy's tags so a listing
tells you which schedule produced them without a second lookup.

### What retention will never delete

A schedule is also an automatic deleter, so the limits on what it can remove
matter more than the limits on what it keeps:

<Columns cols={2}>
  <Card title="Snapshots you took by hand" icon="shield">
    Retention only matches snapshots carrying this policy's
    `snapshot_policy_id`. A snapshot a person created has none and is never a
    candidate, whatever tags it has.
  </Card>

  <Card title="Snapshots something depends on" icon="git-branch">
    A snapshot a volume was created from — including a restore still running —
    is skipped and re-examined on a later run. It becomes reapable once the
    dependent volume is gone.
  </Card>

  <Card title="The single newest snapshot" icon="clock">
    The newest snapshot is exempt from the **age** bound. A volume that could
    not be snapshotted for longer than the window never loses its whole
    history that way.
  </Card>

  <Card title="Anything, while paused" icon="pause">
    `enabled: false` pauses the entire policy — no snapshots taken and none
    deleted. A paused schedule that kept reaping would delete history while you
    were looking at it.
  </Card>
</Columns>

<Warning>
  Resuming a paused policy applies the retention window again on its next run.
  If you lowered `retention_count` while it was paused, everything now outside
  the window is reaped on that run.
</Warning>

### Reading a schedule's state

`GET /v1/snapshot-policies/{policy_id}` returns where the policy is in its
cycle:

| Field         | What it tells you                                                               |
| ------------- | ------------------------------------------------------------------------------- |
| `next_run_at` | When the next snapshot is due.                                                  |
| `last_run_at` | When the policy last fired. Absent until the first run.                         |
| `last_error`  | Why the most recent run produced no snapshot. Empty after a run that succeeded. |

`last_error` is the field to check on a schedule that has stopped producing
snapshots — it carries reasons like an exhausted `snapshots` quota, or a volume
that was mid-extend when the window came around. The **Snapshot schedule** card
prints all three, so a stalled schedule can be read without leaving the console.

<Info>
  A missed window costs **one** snapshot, not one per window missed.
  `next_run_at` is re-stamped to `now + interval_minutes` each time the policy
  fires, never to `previous + interval`, so a schedule that could not run for
  six hours takes a single snapshot when it resumes rather than a catch-up
  burst. Changing `interval_minutes` re-bases the next run off now too, so
  shortening a daily schedule to hourly takes effect within the hour.
</Info>

Deleting a policy detaches the schedule and **keeps** every snapshot it already
took — they become ordinary snapshots you own outright and are never reaped
again. Delete the snapshots themselves if that is what you meant.
