Skip to content

Backups & Point-in-Time Recovery ​

This guide covers backup, restore, and PITR workflows in AYB, based on code in internal/backup/, internal/cli/db_backup.go, and internal/server/backup_admin_handler.go.

Overview ​

AYB supports two backup strategies:

  • Logical backups (pg_dump): Portable SQL-format dumps for cross-version restores and selective table recovery.
  • Physical backups (pg_basebackup): Full binary copies of the data directory for fast, exact-state restores. Required for point-in-time recovery (PITR).

Both strategies store artifacts in S3-compatible object storage with optional server-side encryption.

WAL archival ​

For AYB-managed PostgreSQL, ayb start automatically enables WAL archival when both [backup] and [backup.pitr] are enabled and the PITR configuration is usable. Managed startup generates archive_mode = on, wal_level = replica, and an active archive_command in postgresql.conf. The command embeds the effective absolute path to the running AYB binary and the resolved absolute path to the effective config file before invoking ayb wal-ship.

AYB rewrites the managed postgresql.conf on every start. Because archive_mode is a postmaster-level setting, applying it requires a full PostgreSQL restart, not pg_ctl reload or SELECT pg_reload_conf(). The managed ayb start lifecycle performs that stop/start and applies the generated settings; do not hand-edit the generated file.

For externally managed PostgreSQL, you must install the archive settings yourself in the PostgreSQL configuration:

Add the following to your postgresql.conf:

ini
archive_mode = on
archive_command = '/absolute/path/to/ayb wal-ship --config /absolute/path/to/ayb.toml %p %f'

Both paths must be absolute. Postgres runs archive_command through /bin/sh from the data directory with a minimal environment, so a relative path or a bare ayb will not resolve.

Restart externally managed PostgreSQL after installing the settings so archive_mode takes effect. Once archive_mode is already enabled, changing only archive_command can be applied with a reload.

rpo_minutes is the lag-alert threshold, not a WAL shipping cadence or a guaranteed recovery point. Actual recoverability depends on successfully archived WAL plus retained physical base backups; without a working archive command, recovery is limited to available base backups.

Configuration ​

Backup and PITR settings live in your ayb.toml config file.

[backup] section ​

KeyTypeDefaultDescription
enabledboolfalseEnable the backup subsystem
bucketstring—S3 bucket name (required when enabled)
regionstring"us-east-1"S3 region
prefixstring"backups"Object key prefix for backup artifacts
schedulestring"0 2 * * *"Cron expression for automated backups (daily 2 AM UTC)
retention_countint7Maximum number of backups to retain (0 = unlimited)
retention_daysint30Delete backups older than N days (0 = unlimited)
encryptionstring"AES256"Server-side encryption: "" (none), "AES256", or "aws:kms"
endpointstring""Custom S3 endpoint for MinIO or LocalStack
access_keystring""S3 access key
secret_keystring""S3 secret key

[backup.pitr] section ​

KeyTypeDefaultDescription
enabledboolfalseEnable point-in-time recovery
archive_bucketstring—S3 bucket for WAL archives (required when enabled)
archive_prefixstring""Optional namespace prefix for WAL archive paths
wal_retention_daysint14Days to retain archived WAL segments
base_backup_retention_daysint35Days to retain physical base backups
compliance_snapshot_monthsint12Months to retain compliance snapshots
environment_classstring"non-prod"Environment label (e.g. "prod", "staging")
kms_key_idstring""KMS key ID for WAL encryption
retention_schedulestring"0 4 * * *"Cron expression for retention cleanup
rpo_minutesint5WAL lag-alert threshold only (must be > 0). WALLagChecker.Check raises a wal_archive_lag alert when the newest archived WAL segment is older than this. It is not a shipping cadence or guaranteed recovery point; recoverability depends on successfully archived WAL plus retained base backups.
storage_budget_bytesint640Maximum storage for WAL archives (0 = unlimited)
shadow_modebooltrueSee Shadow mode below
base_backup_schedulestring"0 3 * * *"Cron expression for physical base backups
verify_schedulestring"0 */6 * * *"Cron expression for backup verification

Shadow mode ​

WARNING

shadow_mode defaults to true. shadow_mode does not disable base backups or WAL shipping; it makes AYB refuse actual restore cutover requests with a 409 Conflict error. This lets you validate that PITR inputs are being produced before enabling real restores.

Set shadow_mode = false in your [backup.pitr] config to enable actual point-in-time restores.

CLI commands ​

ayb db backup ​

Trigger an immediate logical backup.

bash
ayb db backup [--database-url <url>] [--config <path>] [--output json]
FlagDescription
--database-urlDatabase URL (overrides config)
--configPath to ayb.toml config file
--outputOutput format: table (default) or json

Output includes: BackupID, Status, ObjectKey, SizeBytes, Checksum.

ayb db backup list ​

List existing backups with optional filtering.

bash
ayb db backup list [--status <status>] [--limit <n>] [--database-url <url>] [--config <path>] [--output json]
FlagDefaultDescription
--status—Filter by status: running, completed, failed
--limit20Maximum number of records
--database-url—Database URL (overrides config)
--config—Path to ayb.toml
--outputtableOutput format: table or json

ayb db restore ​

Restore from a backup.

bash
ayb db restore --from <backup-id-or-key> [--database-url <url>] [--config <path>] [--yes]
FlagDescription
--fromBackup ID or S3 object key to restore from
--database-urlTarget database URL
--configPath to ayb.toml
--yes, -ySkip confirmation prompt

PITR workflow ​

Point-in-time recovery follows a validate → dry-run → execute → monitor sequence.

1. Validate the restore window ​

Before attempting a restore, validate that the target time falls within the available recovery window:

bash
curl -X POST http://localhost:8090/api/admin/backups/projects/{projectId}/pitr/validate \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_time": "2026-03-15T10:30:00Z", "database_id": "db-1"}'

The response includes earliest_recoverable and latest_recoverable timestamps, the base backup that would be used, and estimated WAL bytes to replay.

Validation requirements (from restore_planner.go):

  • At least one completed physical backup must exist.
  • Target time must be between the earliest completed backup and the latest archived WAL segment.
  • WAL segments must form a contiguous chain from the base backup's end LSN to the target.

2. Dry-run the restore ​

Pass "dry_run": true to the restore endpoint to see the plan without executing:

bash
curl -X POST http://localhost:8090/api/admin/backups/projects/{projectId}/pitr/restore \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_time": "2026-03-15T10:30:00Z", "database_id": "db-1", "dry_run": true}'

3. Execute the restore ​

Set "dry_run": false (or omit it) to start the restore:

bash
curl -X POST http://localhost:8090/api/admin/backups/projects/{projectId}/pitr/restore \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_time": "2026-03-15T10:30:00Z", "database_id": "db-1"}'

Returns a job_id for tracking. The restore proceeds through these phases:

PhaseDescription
pendingRestore job created
validatingValidating restore window and backup availability
restoringExtracting base backup, downloading WAL segments, applying recovery
verifyingWAL replay complete, verifying data consistency
ready_for_cutoverRecovery instance ready for application switchover
completedRestore finished
failedRestore encountered an error (check job status for details)

4. Monitor the restore job ​

bash
# List jobs for a project
curl http://localhost:8090/api/admin/backups/projects/{projectId}/pitr/jobs?database_id=db-1 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Get a specific job
curl http://localhost:8090/api/admin/backups/restore-jobs/{jobId} \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Abandon a running job
curl -X DELETE http://localhost:8090/api/admin/backups/restore-jobs/{jobId} \
  -H "Authorization: Bearer $ADMIN_TOKEN"

API endpoints ​

All backup endpoints require admin authentication and are mounted under /api.

MethodPathDescription
GET/api/admin/backupsList backups (query: status, limit, offset)
POST/api/admin/backupsTrigger a new backup
POST/api/admin/backups/projects/{projectId}/pitr/validateValidate PITR restore window
POST/api/admin/backups/projects/{projectId}/pitr/restoreStart or dry-run a PITR restore
GET/api/admin/backups/projects/{projectId}/pitr/jobsList restore jobs (query: database_id)
GET/api/admin/backups/restore-jobs/{jobId}Get restore job status
DELETE/api/admin/backups/restore-jobs/{jobId}Abandon a restore job

Response codes ​

CodeMeaning
200Success
202Backup triggered / restore started
400Invalid parameters or target time
404Job not found
409Shadow mode active (restore refused)
500Internal error
503Backup or PITR service not configured

Fire drill testing ​

A fire drill validates only that a restore plan is viable for a target 5 minutes in the past. It asks the restore planner whether a suitable base backup exists and whether the archived WAL segments form a contiguous chain up to that target.

A fire drill does not restore data, does not launch a recovery Postgres instance, and does not verify any recovered row. A passing drill means the inputs for a restore are present, not that a restore has been proven to succeed.

The FireDrillResult includes:

  • Whether the drill Passed — that is, whether a viable plan was found
  • The RestorePlan (base backup + WAL segments) that would be used
  • Any error encountered during planning

Fire drills run on the schedule configured by verify_schedule (default: every 6 hours).

Retention and storage budget ​

AYB provides multiple retention controls:

  • retention_count: Maximum number of logical backups to keep.
  • retention_days: Delete logical backups older than N days.
  • wal_retention_days: Delete archived WAL segments older than N days (default: 14).
  • base_backup_retention_days: Delete physical base backups older than N days (default: 35).
  • compliance_snapshot_months: Retain compliance snapshots for N months (default: 12).
  • storage_budget_bytes: Cap total WAL archive storage (0 = unlimited).

Retention cleanup runs on the retention_schedule cron (default: daily at 4 AM UTC).

Backup statuses ​

StatusDescription
pendingBackup record created, not yet started
runningBackup in progress
completedBackup finished successfully
failedBackup encountered an error
deletedBackup artifact removed by retention policy

Released under the MIT License.