---
title: "Backups and restore drills"
description: "Declare how much data you can lose and how fast you must restore, check your provider can meet it, and prove it with timed restore drills."
section: "interfaces"
status: "Building for 1.2 · ships as a beta API"
spec: "backup-restore-evidence"
canonical: "https://mainsdk.dev/docs/interfaces/backup"
updated: "2026-10-28"
---

# Backups and restore drills

> Declare how much data you can lose and how fast you must restore, check your provider can meet it, and prove it with timed restore drills.

Status: Building for 1.2 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/backup · Updated: 2026-10-28

`backup` turns "we have backups" into proof: declare your RPO and RTO, let `mainsdk check` test your provider against them, and record timed restore drills as signed evidence.

> **Note:** Built for 1.2 as `mainsdk/backup` and `mainsdk/backup-providers`, with `mainsdk drill restore` and the `backup` gate in `mainsdk check`. The working surface is `runDrill`, `signDrill`, `verifyDrill`, `backupIssues`, `drillControls` and `backupBadge`.

## At a glance

- Status: Building for 1.2 · ships as a beta API
- Phase: 3 (Mobile and regulated)
- Milestone: 1.2 Regulated industries
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/backup-restore-evidence.md
- Category: Reliability and operations

Calls:

- `runDrill({ driver, target, profile, at })`
- `signDrill(result, key)`
- `verifyDrill(evidence, trusted)`
- `backupIssues(profile, declared, drill)`
- `drillControls({ standard, evidence, now })`
- `backupBadge({ evidence, now })`

In main.yaml:

```yaml
backup:
  rpo: 15m
  rto: 1h
  keep: 35d
  drill: { every: 30d, to: scratch }
```

Guarantees, whatever the driver:

- Backups encrypted, copied off-site and immutable for the retention period
- The badge turns amber when no restore drill passed in 30 days
- Restore per tenant, not only all or nothing
- MainSDK never runs or stores backups itself; the provider does


## Drivers

Pick one in main.yaml with `backup.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Local (tests and drills without a provider) | `local` |  |  |
| Neon point-in-time recovery | `neon-pitr` |  |  |
| Supabase point-in-time recovery | `supabase-pitr` |  |  |
| pgBackRest | `pgbackrest` |  | planned driver |
| AWS Backup | `aws-backup` |  |  |
| Object storage with object lock | `object-lock` |  |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-BACKUP-0001` | check | The provider's best RPO (PITR step, else snapshot gap) is above `backup.rpo`. Fix: turn on PITR or raise `backup.rpo` |
| `MAIN-BACKUP-0002` | check | The provider keeps restore points for less than `backup.keep`. Fix: raise the provider retention or lower `backup.keep` |
| `MAIN-BACKUP-0003` | check | The last drill took longer than `backup.rto`. Fix: shrink the restore path or raise `backup.rto` to what a drill measured |
| `MAIN-BACKUP-0006` | check | `backup.rpo`, `backup.rto`, `backup.keep` or `backup.drill` is missing or not a duration. Fix: set durations such as `15m`, `1h`, `35d` |
| `MAIN-BACKUP-0004` | runtime |  |
| `MAIN-BACKUP-0005` | runtime |  |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Backup and point-in-time restore in the db and files contracts | 3 | 1.2 | `backup-restore-evidence#backup-capability` |
| Provider backup settings read | 3 | 1.2 | `backup-restore-evidence#provider-readers` |
| RPO, RTO and retention check | 3 | 1.2 | `backup-restore-evidence#rpo-rto-check` |
| Timed restore drill into scratch | 3 | 1.2 | `backup-restore-evidence#drill-restore` |
| Signed drill evidence | 3 | 1.2 | `backup-restore-evidence#drill-evidence` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

Declare your targets in `main.yaml`, then run a drill.

```yaml title="main.yaml"
db:
  driver: postgres
backup:
  rpo: 15m
  rto: 1h
  keep: 35d
  drill: { every: 30d, to: scratch }
```

```bash
npx mainsdk drill restore --at "2027-07-01T10:00:00Z" --to scratch
```

```text
✓ restored orders-db (Neon branch) in 6m 12s · RTO 1h ✓ · RPO 15m ✓ (PITR window 7d)
✓ evidence signed: .mainsdk/evidence/drill-2027-07-14.json
```

In code, `runDrill`, `signDrill`, `verifyDrill`, `backupIssues`, `drillControls` and `backupBadge` from `mainsdk/backup` are the calls behind the command. A driver's optional `snapshot()` only asks your provider for a snapshot.

## Configuration

Every key below lives under `backup:`.

```yaml title="main.yaml"
backup:
  rpo: 15m          # duration: the most data you can lose
  rto: 1h           # duration: the longest time a restore may take
  keep: 35d         # duration: how long the provider keeps backups
  drill:
    every: 30d      # how often a drill should pass
    to: scratch     # where drills restore; never production
```

| Key | Type | What it does |
| --- | --- | --- |
| `rpo` | duration | Required. Checked against the provider's point-in-time window and snapshot frequency |
| `rto` | duration | Required. A drill that takes longer fails RTO |
| `keep` | duration | Retention you expect the provider to hold |
| `drill.every` | duration | Drill cadence; the trust badge turns amber when no drill passed in 30 days |
| `drill.to` | string | The restore target for drills, `scratch` in every example; never production |

The backup driver follows your data drivers: each `db` and `files` driver declares its backup capabilities (point-in-time window, snapshot frequency, off-site copy, object lock) in its capability profile. How you select a separate backup driver, such as pgBackRest or AWS Backup, is not specified yet.

### Overlays by environment

Production usually needs tighter targets than staging. Put the difference in an environment overlay:

```yaml title="main.env.prod.yaml"
backup:
  rpo: 5m
  rto: 30m
```

See [main.yaml](https://mainsdk.dev/docs/concepts/main-yaml) for overlay order.

## What you get

- A check that your database and file providers can meet the RPO, RTO and retention you declared.
- Timed restore drills that restore into a scratch environment, never production, and delete it afterwards.
- Signed drill evidence your auditors can verify, and a trust badge that turns amber when no drill passed in 30 days.
- No backups run or stored by MainSDK: your provider does that, and `backup` exposes its backup and point-in-time restore through one interface.

The calls, all from `mainsdk/backup`:

| Call | What it does |
| --- | --- |
| `runDrill({ driver, target, profile, at })` | Restores the point `at` into scratch, times it and checks the rows, then deletes the target |
| `signDrill(result, key)` | Signs a drill result as evidence |
| `verifyDrill(evidence, trusted)` | Checks the signature and the trusted keys |
| `backupIssues(profile, declared, drill)` | The findings behind `MAIN-BACKUP-0001` to `0003`: where the provider or the last drill misses your targets |
| `drillControls({ standard, evidence, now })` | The control results (A1.2, A1.3, CC7.5; 8.13, 5.30; PCI 12.10.1) that `assure` reads |
| `backupBadge({ evidence, now })` | The badge state: current, or amber when no drill passed in 30 days |

## Examples

### Check your provider meets your targets

`mainsdk check` reads the provider's backup settings and fails with the gap when they cannot meet your RPO and RTO.

```yaml title="main.yaml"
backup:
  rpo: 5m
  rto: 1h
```

```bash
npx mainsdk check --json
```

With a provider that takes daily snapshots and has no point-in-time recovery, the check fails with a `MAIN-BACKUP-` finding that names the provider setting and the change needed, such as turning on point-in-time recovery.

### Run a restore drill in CI

Drills are free to run locally and in your own CI. A drill restores into a new branch or database, times it, verifies row counts or checksums, and tears the scratch environment down.

```yaml title=".github/workflows/restore-drill.yml"
name: restore-drill
on:
  schedule:
    - cron: "0 4 1 * *"
jobs:
  drill:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm install --frozen-lockfile
      - run: npx mainsdk drill restore --at "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" --to scratch
```

If the restore takes 72 minutes against `rto: 1h`, the drill fails RTO and the evidence records the measured time. How a drill authenticates to your provider in CI is not specified yet.

### Attach drill evidence to an audit

Each drill is signed evidence that `mainsdk assure` includes for SOC 2 (A1.2, A1.3, CC7.5), ISO 27001 (8.13, 5.30) and PCI DSS.

```bash
npx mainsdk assure --standard soc2 --json
```

After a passing drill, A1.3 passes with the drill's signed evidence attached, and its signature verifies.

## Errors and checks

When the provider's backup settings cannot meet the declared RPO or RTO, `mainsdk check` fails with a finding that names the setting to change: `MAIN-BACKUP-0001` (RPO), `MAIN-BACKUP-0002` (retention), `MAIN-BACKUP-0003` (last drill over RTO) or `MAIN-BACKUP-0006` (a target missing or not a duration). At run time, `MAIN-BACKUP-0004` means a drill was asked to restore outside scratch, and `MAIN-BACKUP-0005` means the provider returned no settings. Fix the named provider setting, or relax the target.

A failed drill (RTO missed, checksum mismatch) is recorded in the evidence file with the measured values. The launch report shows the trust badge amber with "restore drill overdue" when the last passing drill is older than 30 days.

## Swapping drivers

Backups follow your data driver. Change the `db` driver and the backup capabilities come from the new driver's profile; your `backup:` block and drills stay the same.

```yaml title="main.yaml"
db:
  driver: neon   # was postgres; point-in-time recovery comes from Neon
```

> **Warning:** Moving data between providers is a migration, not a switch. Run a drill on the new provider before you rely on it: your old provider's backups do not move with you.

## Testing

Point-in-time windows and restores need a real provider, so the drill itself is your test: run `mainsdk drill restore` on a schedule in CI, as in the example above. Each `db` and `files` driver's declared backup capabilities are checked against a test project by its conformance suite; see the [Testing kit](https://mainsdk.dev/docs/interfaces/testing).

## Security and privacy

- MainSDK never holds backup copies or pays for storage; the provider does. A "backup now" call only asks the provider for a snapshot.
- Drills never restore into production, and the scratch environment is deleted at the end.
- Backups are erased for one person by deleting their key, through [`keys`](https://mainsdk.dev/docs/interfaces/keys) and [`privacy`](https://mainsdk.dev/docs/interfaces/privacy).
- Drills are free locally and in your CI.

## FAQ

**Does MainSDK back up my database?** No. Your provider does. MainSDK checks its settings, runs drills and keeps the evidence.

**Why did my badge turn amber?** No restore drill passed in the last 30 days. Run `mainsdk drill restore`.

**Can a drill touch production?** No. Drills restore only into a new branch or scratch database.

## Next

- [Data and records](https://mainsdk.dev/docs/interfaces/db): the stores your backups cover.
- [Encryption keys](https://mainsdk.dev/docs/interfaces/keys): crypto-erasure for backups.
- [The check](https://mainsdk.dev/docs/concepts/the-check): how findings like `MAIN-BACKUP-` are reported.
