Restore & Recovery

Temps restores a managed external service (Postgres, Redis, MongoDB, S3/RustFS) from an S3 backup. Three modes are supported: restore in place, clone the backup into a brand-new service, or point-in-time recovery (PITR) on Postgres. Use it to roll a database back to a known-good state, spin up a copy from a backup, or recover to a precise moment after data loss.


Overview

The restore framework is a generic orchestrator (in temps-backup) that dispatches to per-engine implementations (in temps-providers). It runs from the web console — the Service Restore page drives the entire flow — or directly against the REST API.

What's Included

  • In-place restore (destructive overwrite of the existing service)
  • Clone-to-new-service (provision a fresh service from a backup)
  • Point-in-time recovery (PITR) for Postgres
  • A read-only plan preview before you commit
  • Per-engine capability discovery
  • Disaster-recovery restore from another instance's backups
  • Progress tracking via a restore_runs table you can poll
  • Audit logging of every restore run

Why It Matters

  • Preview exactly what a restore will do before running it
  • Clone production data into a staging service without touching production
  • Recover Postgres to a specific timestamp, transaction, or LSN
  • Restore a backup produced by a different Temps instance
  • Self-hosted — backups and restores stay on your infrastructure

How It Works

  1. Discover capabilities. Ask the service what restore modes it supports with GET /api/external-services/{id}/restore-capabilities. The response also includes a suggested_new_service_name, pre-formatted as {source}-restore-{yyyymmdd-hhmm}, to pre-fill the clone dialog.
  2. Plan (optional but recommended). POST /api/external-services/{id}/restore-plan returns a read-only preview: the engine, target service, source backup, the chosen strategy, the ordered steps, a destructive flag, plus non-blocking warnings and blocking errors. The UI disables the Start button when errors is non-empty.
  3. Start the restore. POST /api/external-services/{id}/restore returns 202 Accepted with a RestoreRunView and spawns a background worker. The {id} in the path is the target service — where the data lands.
  4. Poll for progress. Watch GET /api/restore-runs/{id} (or list a service's runs with GET /api/external-services/{id}/restore-runs) until the run reaches a terminal status.

Under the hood, the orchestrator writes a restore_runs row, then dispatches to the engine's ExternalService trait methods: restore_from_s3 (in-place), restore_to_new_service (clone), or restore_pitr (point-in-time).


Restore Modes

The mode is chosen with a JSON mode discriminator (snake_case). On the restore and restore-plan endpoints the mode fields are flattened into the top-level request body alongside backup_id.

modeWhat it doesRequired fieldsDestructive
in_placeRestores the backup onto the existing service, overwriting current datanoneYes
new_serviceProvisions a fresh service and restores into itname; optional parameter_overridesNo
pitrPoint-in-time recovery (Postgres / WAL-G only)to_new_service (bool), target; new_service_name required when to_new_service is trueWhen to_new_service is false

Per-Engine Capabilities

Each engine declares a RestoreCapabilities object: restore_in_place, restore_to_new_service, pitr, earliest_pitr_time, and latest_pitr_time. The framework default is in-place only.

Enginerestore_in_placerestore_to_new_servicepitr
PostgresYesYesYes
S3 / RustFSYesYesNo
RedisYesNo (default)No (default)
MongoDBYesNo (default)No (default)

When pitr is true, earliest_pitr_time and latest_pitr_time bound the recoverable window (derived from engine archive metadata, e.g. pg_stat_archiver for Postgres).


API Endpoints

MethodPathPurpose
GET/api/external-services/{id}/restore-capabilitiesCapabilities + suggested_new_service_name
GET/api/external-services/{id}/restore-runsRecent restore runs for the service
POST/api/external-services/{id}/restore-planRead-only preview of a restore
POST/api/external-services/{id}/restoreStart a restore (returns 202)
GET/api/restore-runs/{id}Poll a restore run's progress

The restore-plan response (RestorePlan) includes a strategy field with one of these values:

strategyMeaning
walg_restoreRestore via WAL-G base-backup replay (Postgres)
pg_dump_restoreRestore via a pg_dump import (Postgres)
unsupportedThe requested restore is not supported for this engine/backup

Permissions

Each request must carry a valid Temps API key (Authorization: Bearer <temps-api-key>). Endpoints gate on these permissions:

EndpointRequired permission(s)
GET .../restore-capabilitiesBackupsRead
GET .../restore-runsBackupsRead
POST .../restore-planBackupsRead (planning is read-only)
GET /api/restore-runs/{id}BackupsRead
POST .../restore (in_place)BackupsWrite + ExternalServicesWrite
POST .../restore (new_service)BackupsWrite + ExternalServicesCreate
POST .../restore (pitr, in place)BackupsWrite + ExternalServicesWrite
POST .../restore (pitr to new service)BackupsWrite + ExternalServicesCreate

Worked Example

This walkthrough assumes you already have a managed Postgres service (id 42 below) with at least one backup (backup_id 1337) to restore from. The PITR steps additionally require WAL-G backups. Set TEMPS_API_KEY to a key with the permissions listed above.

1. Check what the service supports

curl -s https://your-temps-instance.com/api/external-services/42/restore-capabilities \
  -H "Authorization: Bearer $TEMPS_API_KEY"
{
  "restore_in_place": true,
  "restore_to_new_service": true,
  "pitr": true,
  "earliest_pitr_time": "2026-05-20T00:00:00Z",
  "latest_pitr_time": "2026-05-28T12:00:00Z",
  "suggested_new_service_name": "orders-db-restore-20260528-1204"
}

2. Preview an in-place restore (read-only)

curl -s -X POST https://your-temps-instance.com/api/external-services/42/restore-plan \
  -H "Authorization: Bearer $TEMPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "backup_id": 1337,
    "mode": "in_place"
  }'

The response previews the engine, target_service, source_backup, strategy, ordered steps, warnings, errors, destructive, and an echo of mode. If errors is empty, you're clear to start.

3. Clone the backup into a new service

curl -s -X POST https://your-temps-instance.com/api/external-services/42/restore \
  -H "Authorization: Bearer $TEMPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "backup_id": 1337,
    "mode": "new_service",
    "name": "orders-db-restore-20260528-1204",
    "parameter_overrides": {}
  }'

A 202 Accepted response returns a RestoreRunView. Note its id, then poll:

curl -s https://your-temps-instance.com/api/restore-runs/91 \
  -H "Authorization: Bearer $TEMPS_API_KEY"

4. Point-in-time recovery to a new Postgres service

curl -s -X POST https://your-temps-instance.com/api/external-services/42/restore \
  -H "Authorization: Bearer $TEMPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "backup_id": 1337,
    "mode": "pitr",
    "to_new_service": true,
    "new_service_name": "orders-db-pitr",
    "target": { "kind": "time", "time": "2026-05-27T18:30:00Z" }
  }'

Recovery Targets (PITR)

PITR's target is a kind-tagged object (snake_case). Postgres honors all four variants:

kindFieldRecovers to
timetime (ISO 8601 timestamp)A specific point in time
xidxid (string)A specific transaction id (Postgres)
lsnlsn (string)A specific log sequence number (Postgres)
namename (string)A named restore point created via pg_create_restore_point (Postgres)
{ "kind": "xid", "xid": "7654321" }

Disaster Recovery (Orphan Restore)

You can restore a backup that this Temps instance never recorded — for example, a backup produced by another Temps instance and discovered by scanning the S3 bucket. Instead of backup_id, pass backup_location together with backup_engine and s3_source_id:

curl -s -X POST https://your-temps-instance.com/api/external-services/42/restore \
  -H "Authorization: Bearer $TEMPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "backup_location": "s3://my-bucket/backups/orders-db/base.tar",
    "backup_engine": "postgres",
    "s3_source_id": 3,
    "mode": "in_place"
  }'
FieldNotes
backup_locationRaw S3 URL / key of the backup
backup_engineOne of postgres, redis, mongodb, s3
s3_source_idThe S3 source the location lives in

Restore Runs

Every restore is tracked in the restore_runs table and surfaced by the RestoreRunView. Poll GET /api/restore-runs/{id} to follow progress.

FieldDescription
modein_place, new_service, or pitr
statuspending, running, completed, failed, or cancelled (starts at pending)
phaseprepare, provision, restore, recover, verify, completed, or failed (starts at prepare)
recovery_targetThe PITR target, if any
source_backup_idBackup the restore read from
source_service_idService that produced the backup
target_service_idService the data landed in (set later for clones)
target_service_nameName of the target/new service
error_messageFailure detail when status is failed

The underlying row also tracks parameter_overrides, resume_token, log_id, attempt, and timestamps.

Every restore run is audit-logged as EXTERNAL_SERVICE_RESTORE_RUN with the source/target service, mode, and backup id. Audit writes are best-effort — a failed audit write is logged but does not fail the restore request.


Next Steps

  • Backups — create and schedule the backups you restore from
  • Managed Services — the Postgres, Redis, MongoDB, and S3 services this framework restores
  • Rollbacks — roll back a bad deployment (distinct from data restore)

Was this page helpful?