# Backup and restore

For the Spaceship MariaDB profile, the database backup CLI accepts `--dialect=mariadb`, invokes `mariadb-dump` without MySQL GTID flags, and caps the dump child at 45 seconds. Use [the Spaceship guide](spaceship-supreme.md) for operational limits and restore acceptance.

Backups contain private account, session and ledger data. Store them outside public storage, encrypt them and keep the encryption key separate from backup files. This release provides authenticated streaming encryption and a MySQL dump wrapper; **a real MySQL restore rehearsal is still required on the target platform**.

## Snapshot contents and consistency

Back up the database, the complete application upload tree except reproducible quarantine/temp/logs, the exact source release, and the secrets needed to decrypt stored data. Application `ENCRYPTION_MASTER_KEY`, `APP_KEY_VERSION`, `PASSWORD_PEPPER` and idempotency/session secrets must remain recoverable. The backup-encryption key below is a separate 32-byte random key; never reuse the application master key.

For a consistent database-plus-media recovery point, drain ingress and stop web/worker/scheduler before both snapshots. `mysqldump --single-transaction` supplies a consistent InnoDB database snapshot but does not synchronize external files or permit concurrent schema changes. Do not run migrations/DDL during a dump. Its privilege and consistency requirements are described by [MySQL's mysqldump manual](https://dev.mysql.com/doc/refman/8.4/en/mysqldump.html).

## Create an encrypted database backup

On the trusted backup host, create a mode-0600 key file with 32 random bytes encoded as base64. Record a non-secret key identifier in the operator inventory and securely escrow the key; losing it makes backups unrecoverable. Create a mode-0600 MySQL option file using an account with the documented backup privileges, TLS verification and the intended database host. Both paths must be absolute. Do not pass passwords in command arguments.

Example operator-owned option file (replace placeholders before use):

```ini
[client]
host=database.example.invalid
user=backup_operator
password=REPLACE_WITH_SECRET
ssl-mode=VERIFY_IDENTITY
ssl-ca=/etc/task-app/mysql-ca.pem
```

```sh
node bin/backup.js database --database=telegram_task_earning --defaults-file=/etc/task-app/backup.cnf --key-file=/etc/task-app/backup.key --output=/srv/task-backups/database-2026-09-12.enc
```

The wrapper calls the installed `mysqldump` with single transaction, quick streaming, hex binary encoding, triggers/routines/events, no tablespaces and GTID-purged disabled. It uses gzip plus AES-256-GCM, a fresh random nonce and an authenticated version header. Only a successful client exit publishes the final file; failed dumps remove staging output. Existing output files are never replaced. This is a logical recovery archive, not a replication/PITR bootstrap. Large installations should separately configure and rehearse physical backups and binary-log recovery.

## Media and other files

On a drained host, create a tar archive of the required media using trusted, fixed paths. Write it to private storage with `umask 077`; exclude backups themselves, logs and quarantine, and avoid dereferencing symlinks. Then encrypt it:

```sh
node bin/backup.js encrypt --input=/srv/task-backups/media.tar --key-file=/etc/task-app/backup.key --output=/srv/task-backups/media-2026-09-12.enc
```

After verifying decryption and copying the encrypted backup to protected off-host storage, remove temporary plaintext according to the storage device's retention policy. File deletion alone does not guarantee secure erasure on snapshots or SSDs. Back up application secret files separately with restricted access.

## Restore rehearsal into an empty isolated environment

1. Keep production untouched. Prepare an isolated recovery host/database with outbound Telegram/provider access disabled and **do not start worker/scheduler**. Use a database principal restricted to the recovery schema. Ensure the recorded view/trigger DEFINER accounts exist with appropriate controlled privileges; do not blindly strip or rewrite definers.
2. Decrypt into an empty private recovery directory. Authentication must finish successfully before reading/importing its output:

   ```sh
   node bin/backup.js decrypt --input=/srv/task-backups/database-2026-09-12.enc --key-file=/etc/task-app/backup.key --output=/srv/task-restore/database.sql
   ```

   The default decompressed size cap is 10 GiB; operators can set `--maximum-bytes` explicitly for reviewed larger backups. Wrong key, changed header/ciphertext/tag, truncation or excess size rejects the restore and removes partial plaintext. This command never imports SQL or overwrites existing data.

3. Verify the backup provenance, intended database and target emptiness. Inspect any database-qualified statements or definer declarations before an operator runs the MySQL client with the restricted recovery principal. A SQL dump is executable code: never import an unknown archive. Import into the intended empty schema through the client's `--defaults-file` and explicit database selection. An import failure requires a new isolated rehearsal target and investigation, not disabling constraints.
4. Decrypt the media tar separately. List and review paths before extracting into a new empty directory; reject absolute/traversal paths and unexpected symlinks. Restore its ownership/permissions. Do not extract over an active application tree.
5. Install the matching release and required app secrets. Verify migration/seed checksums, all 77 application tables plus schema metadata, ledger balances, double-entry totals, active bans and media availability. Compare record counts to a snapshot inventory taken while drained. Run a controlled TOTP/login rehearsal and server-side eligibility checks.
6. Inspect pending/processing outbox messages and pending withdrawals against delivery/payout records before restarting automation. A restored backup may contain a job already sent after the snapshot. Do not reset attempt counters, mark payments sent speculatively, or bulk replay messages.
7. Record measured backup duration, restore duration, data loss window (RPO), recovery time (RTO), key identifier, archive hashes, operator and result. Only then schedule a reviewed production cutover. Keep the old database and release recoverable until acceptance completes.

Start with daily backups and a separately retained pre-upgrade copy, then choose retention/RPO/RTO from actual business needs. Alert on missed/failed backups and storage capacity. Rehearse restoration regularly; an encryption roundtrip or successful dump exit alone is not proof of recoverability.
