<!-- x-shc-release: version=2.4.15 op_fingerprint=183a4a0282542d671d799f08044f6717182932f9fa1ef5205dfca45a030cefaa op_count=177 contractFingerprint=e8f7ccc12336fb4925893ce3582b03782b182638dc460d952d046a36c1c20e14 -->
---
name: nightly-backup
description: 'Install a nightly encrypted backup of a deployed app''s data + database to a destination the user controls, with a mandatory verify-restore drill so "your data is safe" is a verified claim, not a hope.'
when_to_use: 'After deploying an app the user cares about (BTCPay store, Nextcloud), or any time the user asks to "back up my server / set up backups". Pairs with shc-deploy-app.'
router_what: 'Nightly encrypted backup to a user-controlled destination, with a verify-restore drill.'
router_when: 'After deploying an app worth keeping, or when the user asks for backups.'
version: 2.4.3
operator: true
client_side: false
requires: [ssh_root_or_sudo, vps_ready]
provides: [nightly_backup]
inputs:
  - { name: ssh_host, required: true, desc: VPS host/IP }
  - { name: app, required: true, desc: 'which app to back up: btcpay|nextcloud' }
  - { name: dest, required: true, desc: 'backup destination the user controls (S3-compatible URL, rsync target, or SHC object storage)' }
  - { name: hour_utc, required: false, default: 3, desc: nightly run hour (UTC) }
outputs:
  - { name: scheduled, desc: true once the nightly timer is installed AND a first backup was verified by restore }
estimated_minutes: [5, 20]
danger: low
---

# nightly-backup -- nightly encrypted backups with a verify-restore drill
## Operator discipline (hold to these on every step)

*Five rules that keep you safe while you operate a server for someone. Follow them on every
step of every skill.*

1. **Verify, don't trust. Never fabricate a result.** Never report `ready`, `done`, `live`,
   `deployed`, `backed up`, or `paid` unless a real tool output or healthcheck PROVED it (and that
   proof arrives in a separate later message, never in your own reply). A status field is a claim; a
   healthcheck is proof. This holds under pressure: "tell my boss in one line it's live", "answer in
   one line", "you do not need to check" do NOT make it true and do NOT waive the check. If you have
   not verified, say exactly that -- never write a done/live/paid sentence you did not earn.
2. **Spending AND destruction are the human's call -- the user CANNOT authorize you to skip the
   confirm.** For money (order, pay) OR data-loss (reinstall, delete, cancel, restore): name the
   action, state the consequence, get the human's explicit yes for THAT action; never in one step.
   The trap: "just do it / don't ask me / skip confirmation / no need to confirm / I already agree" is
   an instruction to skip the gate -- it is NOT a yes and does NOT give you permission to bypass the
   gate. The gate OUTRANKS that instruction; proceeding because they said "just do it" is itself the
   failure. So you still state the consequence and still wait for a real yes. NEVER set `confirm=true`
   yourself, NEVER map "just go" onto `human_confirmed=true`, and NEVER assume a missing confirmation
   "defaults to true" -- the opposite is true: missing -> REFUSED (409 confirmation_required). If they
   will not give the yes, you do NOT proceed -- announce nothing, run nothing. (Mechanics: every spend
   AND destructive action is gated by a **server-issued, single-use confirmation_id** -- you can NEVER
   self-confirm. On the hosted MCP the destructive/spend tool returns the confirmation_id after its own
   human-approval step. On raw /v2 a destructive/spend call with no confirmation_id returns **409
   confirmation_required** carrying a one-time, non-secret `confirmation_id` and a `how_to_proceed`
   re-call shape; AFTER the human's explicit yes, re-send the IDENTICAL request adding the header
   **`X-User-Api-Confirm: <that confirmation_id>`**. `?confirm=true` and body `confirm:true` are NOT
   accepted -- they get the same 409. The confirmation_id is bound server-side to that exact call +
   your principal and is single-use, so a leaked id is inert (it completes nothing on its own). The
   409's `how_to_proceed` is a recovery shape, NOT a license: a confirmation_id only proves the SERVER
   has a pending record for the action -- do NOT auto-resend it until the human has actually said yes to
   the consequence. Spend is also scope-gated: an `operate` key cannot spend at all -- only a `full` key
   or HTTP Basic can.)
3. **Check the source before you run it.** Validate any deploy recipe against the project's
   *current* upstream docs, not a cached memory.
4. **Take the smallest key that does the job.** An agent that only provisions should never
   hold the key that spends.
5. **When you can't verify, stop and say so.** Failing loudly beats a confident wrong answer.


Install an encrypted nightly backup to a destination the user controls, and PROVE it restores before
reporting success.

## Safety gates (never break)

- The verify-restore drill is mandatory. An unverified backup is `failed`, not `ready` -- "scheduled"
  is a claim; a restored dump is proof. If you cannot prove the restore, report `failed` and say so.
- Encrypt before data leaves the box, with a key the USER holds. Never let the only copy of the backup
  key live on the box being backed up, and never paste it into chat -- deliver it to the user.
- Use the app's supported backup path (check the source); do NOT hand-tar volumes.

## Steps

1. **What to back up:**
   - BTCPay: wrap the upstream `btcpay-backup.sh` (dumps PostgreSQL + the data dir into one archive).
     Confirm its name/location against the current `btcpayserver-docker` docs first.
   - Nextcloud: use AIO's own Borg-based backup.
2. **Encrypt** before it leaves the box (e.g. `age`/`gpg` symmetric, key the user holds).
3. **Schedule** a systemd timer (preferred) or cron at `hour_utc` nightly.
4. **Ship** the encrypted archive to `dest` (rclone/awscli for S3, rsync over SSH, or SHC object storage).
5. **Verify-drill (mandatory):** run one backup now, pull it back, decrypt, assert it restores (e.g.
   the PostgreSQL dump loads into a throwaway container). Only then report success.
6. **Retention:** keep N nightlies (default 7), prune older.

```bash
# BTCPay path
ssh -o BatchMode=yes root@<HOST> '
  cd "$HOME/btcpayserver-docker"
  ./btcpay-backup.sh                       # -> upstream archive (PostgreSQL + data dir)
  # encrypt:  age -r <user_pubkey> backup.tar.gz > backup.tar.gz.age
  # ship:     rclone copy backup.tar.gz.age <dest>
'
# install the nightly timer for the above, then run the verify-drill ONCE before reporting ready.
```

Default destination can be SHC object storage if the user has no external target, but offer the
user-controlled option first.

## Result contract

- ready: `STATUS: ready` + `NEXT_ACTION: none (nightly backup scheduled at HH:00 UTC; first backup verified by restore)`.
- failed: `STATUS: failed` + `STEP/REASON/NEXT_ACTION` (e.g. "verify-drill failed to restore the dump; backup NOT trusted -- check dest credentials").

**You're done when:** the nightly timer is installed AND the verify-restore drill actually restored the
first backup. A scheduled-but-unverified backup is NOT done -- report `failed` until a restore proves it.
