---
title: "Restore Channel Postgres"
method: POST
path: "/apps/{app_id}/channels/{channel_id}/postgres/restore"
---

# Restore Channel Postgres

`POST /apps/{app_id}/channels/{channel_id}/postgres/restore`

## Request

- Base URL: `https://api.xhostd.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.xhostd.com/apps/{app_id}/channels/{channel_id}/postgres/restore`
- Auth: none declared

Record a restore of the channel's database and answer 202.

The checks run in one transaction with the record, in this order,
and each refusal records nothing:

1. The channel: 404 for a caller who is not a member.
2. A move that carries the app: 409 maintenance. The check runs
   under the owner lock in its weaker mode
   (``pg_placement.py::lock_owner_no_key``), which conflicts with the
   ``FOR UPDATE`` every ``plan_apply`` producer takes before it
   checks for a restore, so a restore and a plan change of one user
   never both pass.
3. The ``prod`` channel: 403 ``protected_action`` unless a person
   drives the caller or the owner turned agent access on.
4. The typed name: 400 ``invalid_confirmation``.
5. A ``maintenance_channel_id`` with a mode other than
   ``maintenance``: 400 ``maintenance_channel_needs_maintenance``.
6. Degraded mode: 503 ``postgres_unavailable``.

A ``request_id`` the ledger already holds then goes straight to the
core's verb, which replays the recorded restore, or answers 409
``request_id_conflict`` for an id recorded for another call. A new
request takes the channel's row lock
(``channels/service.py::lock_channel``), which the tenant deploy and
rewind, the admin rebuild, the operator redeploy, the push worker,
and the transfer accept take before they read the restore holds, and
meets:

7. The owner, read again under the channel lock: 409 when an
   ownership transfer changed it since the read of check 1.
8. The snapshot: 404 ``snapshot_not_found`` or
   ``snapshot_host_gone``, and 409 ``snapshot_out_of_window``.
9. The page channel, when the body names one
   (``proxy/maintenance.py::maintenance_target_refusal``): 400
   ``maintenance_channel_not_found`` for a channel that does not
   exist or has another owner, ``maintenance_channel_not_static``,
   or ``maintenance_channel_not_deployed``.
10. The core's verb (``postgres/api.py::restore_channel_database``):
   409 when the database is not ready, and 409
   ``restore_in_flight`` while another restore of the channel is
   active.

Once the core records the restore, the same transaction cancels the
channel's queued deploys and rewinds
(``deploy/dispatch.py::cancel_queued_for_restore``). A running deploy
and a queued rehome are left alone: the restore waits for them
(design/PG-FLEET.md, "Guards while a restore is active").

The body's ``during_restore``, ``read_only`` by default,
``maintenance``, or ``live``, and its ``maintenance_channel_id`` are
parameters of the recorded command, so a replay that names another
mode or another page channel answers ``request_id_conflict``. The
answer is the
restore's status and the ids of the deploys its start cancelled,
with ``Location`` naming its status route. A replay reads the
cancelled deploys back and cancels nothing. The route then nudges the
restore dispatcher.

## Path parameters

- `app_id` string, uuid, required
- `channel_id` string, uuid, required

## Headers

- `authorization` string, nullable

## Request body

- RestoreRequest
  - `confirm_db_name` string, required
  - `snapshot_id` string, uuid, required
  - `request_id` string, uuid, nullable
  - `during_restore` 'read_only' | 'maintenance' | 'live': What the channel's app can do with its database while the restore runs, from the claim to the swap. ``read_only``: the ``hold`` step makes every new transaction of ``r_<ch32>`` in ``ch_<ch32>`` read-only, so a write fails with SQLSTATE 25006 rather than vanish at the swap. ``maintenance``: the same hold, and the edge serves a page in place of the app (``proxy/maintenance.py``), the restore's ``maintenance_channel_id`` parameter naming a static channel for the page or ``None`` for the platform's own. ``live``: the app keeps writing, and the swap discards those writes.
  - `maintenance_channel_id` string, uuid, nullable

## Response `202`

Successful Response

- RestoreSubmitResponse: The answer to a restore's start: its status, and the deploys the start cancelled. ``cancelled_deploys`` names the channel's queued deploys and rewinds that the start turned ``cancelled``, oldest first (``deploy/dispatch.py::cancel_queued_for_restore``). A replay of the request names these, and any row the pickup fail-safe cancelled for the restore (``deploy/dispatch.py::cancelled_for_restore``).
  - `restore_id` string, uuid, required
  - `app_id` string, uuid, required
  - `channel_id` string, uuid, required
  - `snapshot_id` string, uuid, required
  - `during_restore` 'read_only' | 'maintenance' | 'live', required: What the channel's app can do with its database while the restore runs, from the claim to the swap. ``read_only``: the ``hold`` step makes every new transaction of ``r_<ch32>`` in ``ch_<ch32>`` read-only, so a write fails with SQLSTATE 25006 rather than vanish at the swap. ``maintenance``: the same hold, and the edge serves a page in place of the app (``proxy/maintenance.py``), the restore's ``maintenance_channel_id`` parameter naming a static channel for the page or ``None`` for the platform's own. ``live``: the app keeps writing, and the swap discards those writes.
  - `maintenance_channel_id` string, uuid, nullable, required
  - `status` 'queued' | 'extracting' | 'loading' | 'swapping' | 'succeeded' | 'failed', required
  - `waiting` string, nullable, required
  - `queue_ahead` integer, nullable, required
  - `activity` string, nullable, required
  - `progress` RestoreProgress, required: How far a restore's current step got. While the restore extracts, ``phase`` names the extract's phase: ``fetch``, ``refetch``, ``replay``, or ``dump``. ``bytes`` counts the bytes the database server has fetched from the backup, or written to the snapshot's dump, so far, and ``segments`` counts the log files the ``replay`` phase has replayed. ``waiting`` names why the extract waits for the database server, such as ``extraction_busy``. ``items_done`` of ``items_total`` counts the snapshot's entries loaded. A field the step does not report is null.
    - `phase` string, nullable, required
    - `bytes` integer, nullable, required
    - `segments` integer, nullable, required
    - `waiting` string, nullable, required
    - `items_done` integer, nullable, required
    - `items_total` integer, nullable, required
  - `error_code` string, nullable, required
  - `message` string, nullable, required
  - `notice` string, required
  - `submitted_at` string, date-time, required
  - `started_at` string, date-time, nullable, required
  - `updated_at` string, date-time, required
  - `finished_at` string, date-time, nullable, required
  - `cancelled_deploys` string[], required

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/xhostd/apis/xhostd-public.md) · [All operations](https://skmtc.dev/xhostd/apis/xhostd-public/llms.txt) · [OpenAPI document](https://skmtc.dev/xhostd/apis/xhostd-public/revisions/bf439c4d1789?raw)
