# 09 — Deployment and Backup Plan

## 1. Target environment (owner-controlled cPanel/WHM server)
The owner controls the server, so versions and configuration are choices, not constraints. **None of the following is verified yet** (no server access from this session).

| Item | Target | Status |
|---|---|---|
| OS | Whatever the owner's cPanel runs (AlmaLinux/Rocky 8/9 typical) | Unknown — owner to report |
| cPanel/WHM | Current stable tier | Unknown |
| PHP | 8.4 minimum, 8.5 if all dependencies support it, via EasyApache 4 / MultiPHP | Unknown whether installed |
| Database | MariaDB 11.4 LTS or 11.8 LTS, chosen for compatibility with the OS and cPanel version | Unknown — owner to confirm what WHM supports |
| Dev parity | Local PHP and MariaDB installed in Phase 1 with the same PHP minor and MariaDB major as production | To do |

If the preferred MariaDB LTS is not yet supported by the installed cPanel, choose the newest supported LTS and update ADR-0003; do not lower below a maintained release.

## 2. Directory and account layout
- Dedicated cPanel account (or subdomain) per environment (staging, production). HTTPS via AutoSSL (wildcard if tenants get subdomains: needs DNS validation, so either cPanel DNS or an ACME DNS plugin).
- Document root → `…/gymos/public`. Everything else under `…/gymos/` outside the docroot: `app/ config/ storage/ keys/ vendor/ database/ cron/`.
- PHP-FPM pool per account with its own system user; `open_basedir` set to the app directory and temp; `expose_php=Off`, `display_errors=Off`, `session.*` handled by the app, `disable_functions` minimal (do not require `exec`; see §6).
- File permissions: directories 0750, files 0640, `keys/` 0700 and key files 0600, owned by the PHP user; `storage/` writable by PHP user only; web server user cannot read `keys/`.

## 3. Server verification checklist (owner runs and reports; recorded in STATUS.md)
```
php -v ; php -m                           # versions + extensions
php -r "var_dump(extension_loaded('sodium'), function_exists('password_hash'), defined('PASSWORD_ARGON2ID'));"
php -i | grep -E "memory_limit|max_execution_time|upload_max|post_max|disable_functions|open_basedir"
mariadb --version  (or mysql --version)
mariadb -e "SELECT @@version, @@innodb_default_row_format, @@character_set_server, @@sql_mode;"
mariadb -e "SHOW GRANTS;"                 # can we create separate DB users?
apachectl -M | grep -E "rewrite|headers|expires|deflate"
whmapi1 version  (or cat /usr/local/cpanel/version)
cat /etc/os-release
crontab -l ; df -h ; free -m
```
Required before Phase 1 sign-off: PHP ≥ 8.4 with the extensions in 03 §1; MariaDB LTS; `mod_rewrite` and `mod_headers`; ability to create 4 DB users (app, migrator, maintenance, backup; see §11) and a cron job.

## 4. Environments and releases
- dev (local) → staging (cPanel, same stack) → production.
- Release = versioned archive built on dev/CI: `app/`, `vendor/` (from `composer install --no-dev --optimize-autoloader`), `public/`, `database/`, checksums + manifest. No Composer/Node on the server.
- Deploy: upload to `releases/<version>/`, run `php cron/cli.php diagnose`, `php cron/cli.php migrate --dry-run`, backup, `migrate`, switch symlink `current → releases/<version>`, smoke test, keep previous release for rollback. Migrations are forward-only with documented down-notes; destructive migrations need explicit two-step releases.
- Installer (web wizard, Phase 11) and CLI upgrader both call the same migrator; installer self-locks after use.

## 5. Scheduled jobs (cPanel cron)
```
* * * * *   /usr/local/bin/php /home/ACCOUNT/gymos/current/cron/run.php  >> /home/ACCOUNT/gymos/storage/logs/cron.log 2>&1
```
`run.php` enforces a time budget (≈50 s), a file/DB lock against overlap, a heartbeat row, and runs: queue worker, scheduler (renewals, reminders, recurring billing, reconciliation, retention), backups. CLI-only (`php_sapi_name() === 'cli'`); no web-triggered cron. Documented limits: cron minimum 1-minute granularity; jobs must be short/resumable; latency up to ~1 min.

## 6. Backups
### 6.1 What is backed up
Database (all tenants), `storage/uploads` (encrypted documents), `storage/branding`, configuration (excluding the master key), release manifest. **The master key is never in these archives.**

### 6.2 Database export — two independent methods
1. **`mysqldump`/`mariadb-dump`** (when `proc_open`/`exec` is permitted and the binary is found): `--single-transaction --routines --triggers --events --hex-blob`, streamed through compression and encryption without writing plaintext to disk where possible.
2. **PHP-native streaming exporter** (used when command execution is disabled, or on request): a separate implementation using PDO with unbuffered queries; emits schema (`SHOW CREATE TABLE`) and data in chunked multi-row `INSERT` statements to a stream; resumable cursor by primary-key range for large tables so it fits execution-time/memory limits across several cron runs; consistent snapshot via `START TRANSACTION WITH CONSISTENT SNAPSHOT` where a single run suffices, otherwise documented per-table consistency caveat with a maintenance-window option. PDO cannot invoke `mysqldump`; the two methods are different code paths and both have tests.
Both produce the same logical format family (SQL), verified by restoring into a scratch database and comparing row counts and checksums per table.

### 6.3 Archive, encryption, retention
- Archive: compressed (zip/tar.gz) with manifest (file list, sizes, SHA-256, app version, schema version, method used).
- Encryption: libsodium secretstream or age-style envelope with a **backup key distinct from the application master key**; backup key escrow kept offline with the owner. Loss of the backup key = loss of backups; a recovery drill is part of verification.
- Schedule (default): DB daily, files daily, weekly full verification; configurable per platform. Retention (default): 7 daily, 4 weekly, 6 monthly. Local copy keeps the last 2 only.
- **Off-site:** at least one protected destination in a different failure domain than the hosting account (S3-compatible bucket with object-lock/versioning, or SFTP to another provider). Credentials stored encrypted (C4). POPIA cross-border note: prefer an SA/adequate-region destination (06 §8). Destination decision D-05.
- Backup is independent of SMTP failover.

### 6.4 Monitoring
`backup_runs` records method, size, checksum, destination, status, duration, verification time. Health check alerts if the last success is older than the schedule × 1.5, size deviates sharply, upload fails, or restore test is overdue (> 30 days).

## 7. Restoration
Documented runbook (to be written alongside M21, rehearsed on staging):
1. Put the app in maintenance mode.
2. Provision matching stack (same PHP minor, MariaDB major).
3. Obtain archive + backup key; verify checksum and manifest signature.
4. Restore files; create empty DB; import SQL (mysql client or the PHP-native importer, which is a separate tested tool for no-CLI hosts).
5. Place master key; run `diagnose` (decrypt sample C3 record, verify audit hash chain, migration version).
6. Reconcile jobs (pause cron; re-queue safely, prevent duplicate emails/payments from replayed queue: `jobs`/`email_outbox` states are checked).
7. Restore requires two-person confirmation for platform admins (record both).
**A backup counts as reliable only after a successful restore test** (`restore_tests` rows), run automatically into a scratch DB monthly and manually before go-live and after major schema changes. Both export methods must each pass restore.

## 8. HTTPS and headers
Force HTTPS (HSTS after staging validation), TLS 1.2+, `Secure` cookies, headers: CSP (nonce), `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, `frame-ancestors 'none'`, `Cache-Control: no-store` on authenticated pages. SPF/DKIM/DMARC for every sending domain (tenants using their own domain need their own records; documented in the SMTP setup screen).

## 9. Shared-hosting-style limitations to plan around (even on an owner-run server)
Execution time and memory limits per PHP-FPM pool · cron granularity 1 min · no persistent worker · disk quotas (uploads + backups staging) · DB size/connection limits · `exec` possibly disabled · outbound ports to SMTP/off-site might be blocked by host firewall (verify 465/587, 443, 22) · PHP-FPM process count under burst · log rotation. Each is checked by `diagnose` and listed in KNOWN-LIMITATIONS.

## 10. Future VPS portability
Business logic is independent of cPanel. Moving to a VPS replaces: cron → systemd timers or a long-running worker (`cron/run.php` loop mode), local storage → object storage adapter, session/cache adapters (DB → Redis optional). Interfaces for Storage, Cache, Queue, Mailer, Clock are defined in Phase 1 so adapters can be swapped.

## 11. DB users (least privilege)
| User | Grants |
|---|---|
| `app` | SELECT/INSERT/UPDATE/DELETE on application tables (no DDL); on `audit_log`: INSERT, SELECT only |
| `migrator` | DDL; used only by CLI migrate |
| `maintenance` | Audit/retention purge, only from CLI |
| `backup` | SELECT, LOCK TABLES, SHOW VIEW, TRIGGER, EVENT (read-only) |
Hosts restricted to `localhost`. If separate DB users cannot be used, hash-chain verification still detects audit tampering (cannot prevent it): recorded as a limitation.
