# Backups & restore

Every environment has durable backup records. A backup is a PostgreSQL
custom-format archive whose readability is checked with `pg_restore --list` and
whose bytes are fingerprinted with SHA-256. A restore replays a backup into a
target environment and records **evidence**: how many relations and
RLS-protected relations were restored, and the SHA-256 of the evidence record —
so "the backup works" is something you can read, not assume.

Archives are produced by the `scribase-backup` operator tool (see
[Run in your own cluster](https://docs.scribase.com/docs/self-hosting.md)); these routes manage the records, trigger
new backups, and run restores.

---

## GET /v1/organizations/{org}/projects/{project}/environments/{env}/backups

Lists the environment's backups, newest first. Optional `?limit=` (1–200).

```json
{
  "items": [
    {
      "backup_id": "nightly-2026-09-23",
      "environment": { "organization_id": "acme", "project_id": "store", "environment_id": "production" },
      "status": "available",
      "format": "custom",
      "size_bytes": 482133504,
      "created_at_epoch_seconds": 1790121600,
      "sha256": "9f2b...",
      "object": { "bucket": "scribase-backups", "key": "scribase/acme/store/production/9f2b....dump", "uploaded_at_epoch_seconds": 1790121900 },
      "verified_at_epoch_seconds": 1790122200
    }
  ]
}
```

`status` is `pending`, `available`, `failed`, or `deleted`. `sha256`, `object`,
and `verified_at_epoch_seconds` appear once the archive exists, is uploaded, and
is verified.

## POST /v1/organizations/{org}/projects/{project}/environments/{env}/backups

Records a new pending backup. The `Idempotency-Key` header becomes the backup
ID, so retrying the same request never creates a second backup.

```sh
curl -X POST "$SCRIBASE_API_URL/v1/organizations/acme/projects/store/environments/production/backups" \
  -H "Authorization: Bearer $SCRIBASE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: manual-2026-09-23' \
  -d '{}'
```

## GET /v1/organizations/{org}/projects/{project}/backups/{backup_id}?environment={env}

Reads one backup. A backup ID is unique within its environment, so the
environment is a required query parameter.

## POST /v1/organizations/{org}/projects/{project}/backups/{backup_id}/restore?environment={env}

Restores the backup from `env` into `target_environment_id` in the same
project. **Headers required:** `Idempotency-Key`.

```sh
curl -X POST "$SCRIBASE_API_URL/v1/organizations/acme/projects/store/backups/nightly-2026-09-23/restore?environment=production" \
  -H "Authorization: Bearer $SCRIBASE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: restore-drill-2026-09-23' \
  -d '{"target_environment_id": "restore-drill"}'
```

**Response:**

```json
{
  "id": "rst_...",
  "source": { "organization_id": "acme", "project_id": "store", "environment_id": "production" },
  "target": { "organization_id": "acme", "project_id": "store", "environment_id": "restore-drill" },
  "source_backup_id": "nightly-2026-09-23",
  "status": "restored",
  "created_at_epoch_seconds": 1790125000,
  "relation_count": 64,
  "rls_relation_count": 18,
  "evidence_sha256": "c41a...",
  "restored_at_epoch_seconds": 1790125410
}
```

`status` is `pending`, `restored`, or `failed`. The evidence fields are present
once the restore has completed.

Restore into a separate environment first (a *restore drill*), check the
evidence and your application against it, then promote — never restore over
production blind.

---

## SDK

```ts
const ref = { organizationId: 'acme', projectId: 'store', environmentId: 'production' };

const backups = await scribase.backups.list(ref);
const backup = await scribase.backups.create(ref, { backupId: 'manual-2026-09-23' });
const { restore } = await scribase.restores.start(ref, {
  backupId: 'nightly-2026-09-23',
  targetEnvironmentId: 'restore-drill',
});
```

## Console

Each environment's **Backups** page lists backups with their verification
state and starts restores.
