# Operations

Persistence, failover, and tuning for a self-hosted arcade.

## Machines

| Role | Runs | Clone? |
|---|---|---|
| **core** | PostgreSQL, Redis, Caddy | no — one only |
| **app** | `quantum-arcade` | yes, freely |
| **standby** | PostgreSQL + `standby.sh follow` | no — one is enough |
| **lightning** | Alby Hub | no — firewalled |

## Backups

PostgreSQL is the only thing that cannot be rebuilt. The app binary embeds its
own client, and Redis holds only sessions and leases, which regenerate.

```bash
sudo mkdir -p /var/backups/quantum-arcade
./ops/backup.sh init          # once
sudo cp ops/arcade-backup.{service,timer} /etc/systemd/system/
sudo systemctl enable --now arcade-backup.timer
```

Every five minutes it captures a compressed snapshot, keeps a rolling 24 hours
(288 snapshots), and prunes the rest.

**Every dump is checked before it replaces the previous one** — size and format
header. A backup script that reports success on a truncated file is worse than
no backup, because it converts a recoverable outage into silent data loss
discovered only when it is needed.

### Prove it works

```bash
./ops/backup.sh verify
```

Restores the newest snapshot into a scratch database and asserts the ledger
sums to zero — the same invariant the live system checks on every request.
Schedule it nightly:

```bash
sudo cp ops/arcade-verify.{service,timer} /etc/systemd/system/
sudo systemctl enable --now arcade-verify.timer
```

A backup nobody has restored is a rumour.

## Standby

A second machine that continuously restores the newest backup and waits.

```bash
sudo cp ops/arcade-standby.service /etc/systemd/system/
sudo systemctl enable --now arcade-standby
./ops/standby.sh status
```

It restores into a shadow database and swaps names only after verifying the
ledger balances, so the standby is never mid-restore when you need it and never
promotes a corrupt copy.

### Pulling the plug

On the standby:

```bash
./ops/standby.sh promote
```

It fetches the newest backup, verifies the ledger, and starts the arcade. It
does not contact the dead machine, because in the situation this exists for the
dead machine is not answering.

Three things it deliberately does not do, because they are unsafe to automate:

1. **Repoint the endpoint.** DNS or the load balancer's upstream list. Until
   that happens players still reach the dead machine.
2. **Confirm the old machine is down.** Two live instances writing to different
   databases diverge, and the result cannot be merged — both ledgers will be
   internally valid and mutually contradictory.
3. **Let players in before checking `/api/health`.** It must report a zero
   ledger sum.

### What you lose

Up to one backup interval — five minutes of play. Rounds in flight at the
moment of failure are refunded automatically by the reconciler once the
standby is live, because their stakes were debited but never settled.

## Lightning is different

**Do not restore an Alby Hub backup the way you restore the database.**

Lightning channel state is not a snapshot you can roll back. Publishing an old
channel state is interpreted by your counterparty as an attempt to cheat, and
the penalty mechanism can take the entire channel balance. Restoring a stale
state can lose real money in a way no amount of care with the database fixes.

Follow Alby Hub's own backup and recovery procedure. Keep the seed phrase
offline and separate from the machine. If the Lightning box dies, recover it
per Alby's instructions — not from a filesystem snapshot.

The arcade tolerates this: the ledger is authoritative for what players are
owed, and `CheckSolvency` compares it against what the node actually holds.

## Kernel tuning

```bash
./ops/tune-kernel.sh check     # what is below target
sudo ./ops/tune-kernel.sh apply
```

Every value is tied to a measured limit, documented inline. The ones that
matter most:

| Setting | Why |
|---|---|
| `fs.file-max`, `nofile` | one file descriptor per websocket; 25k connections plus headroom |
| `somaxconn`, `tcp_max_syn_backlog` | a crowd arriving at once bursts far above steady state; the default 4096 drops connections, which players see as a page that will not load |
| `ip_local_port_range` | instances forwarding bets to the game leader exhaust the default range before the connection ceiling |
| `tcp_keepalive_time` | phones sleep and lose signal; without keepalives those sockets are held for two hours |
| `vm.swappiness` | swapping a database's working set is worse than reclaiming page cache |

Do not raise the buffer sizes further without a measurement. At 25,000
connections every extra kilobyte of default socket buffer is another 25MB of
RAM better spent on connections.

## Daily checks

```bash
curl -s localhost:8080/api/health | jq          # ledger sums to zero
./ops/standby.sh status                         # standby is current
systemctl status arcade-backup.timer            # backups running
journalctl -u arcade-verify --since yesterday   # last restore test passed
```

The one number that matters is `ledger_sum_msat`. It is zero or the platform
is telling you it is broken.
