# Deployment, backups and a new app: how S19 is built

What S19 built (plan CORE §3.16, §3.17, §6, §7; TuteDesk's release and backup posture, TuteLoop's backup commands,
25 October 2026; CORE-D9). The rules: **the server can make a backup but never read one**; **every release backs up,
then runs the deploy steps in order**; **a new app is a release renamed by one command, with no trace of the old name
outside an allowed line**; **drift is a foundation file that differs from the release, unless recorded**. The
operator's side: [../DEPLOYMENT.md](../DEPLOYMENT.md), [../BACKUP.md](../BACKUP.md).

## Backups (`App\Support\Backup`)

| Piece | What it does |
|---|---|
| `BackupKey` | `make()` a key pair (X25519); `public()` from `BACKUP_PUBLIC_KEY`; `pair()` from the private half, at restore |
| `BackupCipher` | `"BAK1"`, a fresh key sealed to the public half (`crypto_box_seal`), then `secretstream` pieces of 1 MB, each with its length; the last marked final. Refuses another key, a changed byte, a piece moved or dropped |
| `BackupFile` | a backup read from its name, `backup-<nightly|predeploy>-<UTC stamp>.bak`: never from file times; anything else in the folder isn't one |
| `Retention` | the names to remove: kept every one for the days, then the newest nightly of each week and month (`backup.keep`) |
| `Backups` | `run(kind)`: `pg_dump -Fc`, each folder of `backup.folders` as a tar, a manifest, packed, encrypted to `….part`, renamed, mode 0600, copied to the remote. `all()`, `newest()`, `prune()` (here, and on the remote unless `remote_prune` is off). `restore(name, private, into, database, folders)`: `pg_restore` in one transaction (`--clean` over the live database, or into a new one `createdb` makes), then each folder swapped |

The tools run through Laravel's `Process` (the password in `PGPASSWORD`, never on the command line), each named in
`backup.bin`. The commands: `backup:key`, `backup:run [--kind]`, `backup:list`, `backup:prune [--dry-run]`,
`backup:restore <name> [--into] [--key-file] [--database-only|--files-only] [--force]`. Scheduled: `backup:run` at
02:00 and `backup:prune` at 02:50, each `when()` backups are set up.

The readiness check **Backups** (`Readiness::backups`): `ok.fresh` (under `stale_hours`, a remote named), `warn.old`,
`warn.one_copy` (no remote), `fail.none` (set up, nothing in the folder), `fail.not_set_up` (production),
`off.not_set_up` (elsewhere).

| Setting (`config/operations.php` `backup`) | Shipped |
|---|---|
| `path` (`BACKUP_PATH`), `public_key` (`BACKUP_PUBLIC_KEY`) | none: off |
| `remote` (`BACKUP_REMOTE`), `remote_prune` (`BACKUP_REMOTE_PRUNE`) | none; true |
| `folders` | `vault` (`FILE_VAULT_ROOT`), `private` (`storage/app/private`) |
| `keep` | 14 days, 8 weeks, 12 months |
| `stale_hours` | 26 |
| `work` (`BACKUP_WORK`) | `storage/app/backup-work` |
| `bin` | `pg_dump`, `pg_restore`, `createdb`, `tar`, `rclone` |

## The deployment kit (`deploy/`)

| File | What it is |
|---|---|
| `deploy/server/release.sh` | one release: packages, the build, `backup:run --kind=predeploy`, `app:deploy`; stops at the first failure |
| `deploy/staging-cpanel.env.example` | what differs for staging on cPanel: the database queue, worked by the scheduler |
| `deploy/production.env.example` | the environment that differs from development, each secret a `<…>` |
| `deploy/server/supervisor.conf` | the queue's workers in production, one program per pool with workers |
| `deploy/launchd/{server,queue,schedule}.plist` | the dev agents, with `{company}`, `{short}`, `{port}`, `{dir}`, `{home}`; `queue` is rendered once per running pool with `{pool}`, `{timeout}`, `{suffix}`; PHP pinned (the house rules) |

## The queue's pools (`config/queue.php` `pools`)

| Pool | Timeout | Workers | What runs there |
|---|---|---|---|
| `default` | 120 s | 2 | E-mail, file scans, and anything not sent elsewhere |
| `bulk` | 1800 s | 1 | Imports and their invitations, audit exports, purges, data copies and erasures |
| `ai` | 300 s | 0 | Nothing yet: Tutela |
| `ocr` | 600 s | 0 | Nothing yet: Lector |

On a host with no supervisor, such as cPanel for staging, `QUEUE_VIA_SCHEDULER=true` has the scheduler start a worker each minute for every running pool, the default first; pools then share that worker, so a long job can hold up the next. `retry_after` is 1860 seconds, above the longest timeout, so a running job isn't handed to a second worker; the cost is that a job whose worker
died waits that long before it runs again. `after_commit` is on for Redis, because Redis isn't in the database's transaction. A pool's
`workers` of 0 starts nothing, and a job sent there is reported stuck by the readiness check. `QueueGuardTest` holds all of this, and that every job
trying more than once has a backoff.

`app:agents <port> [--write]` (`AppAgentsCommand::render`) fills them from `Identity` and the app's folder. The starter
pack itself has no agent: `launch.json` serves it on 8020, the mockups on 8021, the docs on 8022 (the
registry's block).

## A new app (`App\Support\Core\Rename`, `app:rename`)

`Rename::run(root, short, with)` on a tree that is a release as released (`core.json`, else refused unless `--force`):

1. reads the templates (`reji/docs/templates/APP-{PROJECT,README,CHANGELOG}.md`);
2. removes `Rename::RECORDS`: the starter pack's plans, tasks, journey, plan mockups, templates and runbooks;
3. in every text file (not `Rename::SKIP`), `renamed()`: `TuteCore`, `tutecore`, `TUTECORE` and `Tutecore` <!-- brand-guard: allow -->
   to the short name in the same case, except on a line carrying `brand-guard: allow`; with `--port`, the dev ports
   (8020–8022) in `launch.json` and `.env*`; with `--product`, `BRAND_PRODUCT`;
4. renames files and folders named for the starter pack, deepest first;
5. writes `PROJECT.md`, `README.md`, `CHANGELOG.md` from the templates, `composer.json`'s description, and
   `config/core.php` (`version`, `renamed`);
6. writes `core.json` again, in the app's names (`renamed`), so `core:status` compares hashes as they are.

`Rename::left()` lists every trace still there outside an allowed line; `app:rename` prints it and fails while there is
one. The name is `Rename::FROM`. A line may carry `brand-guard: allow` only where it must name the starter pack: the
retired list (`config/brand.php`), the brand guard's fixtures, the vault label's migration, the records of where an app
started.

## Drift (`App\Support\Core\Foundation`, CORE-D9)

`Foundation::DIRS` (`app bootstrap config database deploy lang resources routes tests`) less `Foundation::OWN`
(`config/core.php` and what the build generates) is the foundation. `core:manifest` writes `core.json` (the version and
each file's sha256) in a release's commit (`../runbooks/release.md`); `core:status` lists each file changed or missing
since and fails when any is, leaving out what the app added.

## The vault's label

`FileVault::keyId()` names a master key with a neutral label, `file-vault|` (playbook §3.1 rule 2); the migration
`2026_10_25_000500_neutral_vault_label` relabelled the files stored under the old one, once (rule 6).

## The guards

| Guard | Test |
|---|---|
| A backup holds the database and the folders, opens only with the private half, restores into a new database and over the live folders | `Backup/BackupTest` |
| A changed byte, a moved or dropped piece, another key: refused | `Backup/BackupTest` |
| Kept by the date in the name; a pre-deployment backup by the days only | `Backup/BackupTest` |
| Copied off the server; pruned there only when allowed | `Backup/BackupTest` (rclone faked) |
| Only a backup in its folder restores; into a new database only; a failed run leaves nothing half-written | `Backup/BackupTest` |
| Each state of **Backups**, production included | `Operations/ReadinessTest` |
| The rename: every case, file names, the ports, the records gone, the templates, the release recorded; once, on a release as released | `Core/RenameTest` |
| This whole tree, renamed, leaves no trace outside an allowed line | `Core/RenameTest` |
| `core:status`: nothing after a rename, then each change and each missing file | `Core/RenameTest` |
| The agents render whole, valid, in the app's folder | `Core/RenameTest` |
| The procedure end to end, on the release itself | the rehearsal (`reji/tasks/2026-10-25-S19-deployment-and-rename.md`) |
