Files
wownerogue/docs/DEPLOY_INSTANCES.md
T
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

271 lines
14 KiB
Markdown

# Two-instance deployment runbook
Two independent instances of the same codebase run behind Nginx Proxy Manager (NPM), each with its
own service identity, database, environment file, wallet, and chain.
| Instance | Domain | Port | Chain | Entry | Payouts | Leaderboards |
|----------|--------|------|-------|-------|---------|--------------|
| `wownerogue` | play.wowne.ro | 3000 | Wownero mainnet | free play and purchased service credits | disabled | Pleb and paid prestige |
| `monerogue` | monerogue.app | 3001 | Monero stagenet | free play and direct test-coin entry | enabled, direct solo only | Pleb and Hall of Champions |
NPM forwards both domains to these ports and terminates TLS, so this runbook contains no
reverse-proxy configuration.
## Scope and legal boundary
Such Software operates only these two domains, and only within the scopes encoded in
`src/config/operatedProductProfiles.js`.
`play.wowne.ro` sells non-redeemable service credits for leaderboard and prestige play. It offers no
prize, payout, or cash-out and is not offered or marketed as gambling. Classification depends on
applicable law.
`monerogue.app` is a no-real-value test surface. Its coins are Monero stagenet test data, not money.
Never send mainnet XMR to it, and never enable crypto-match payouts on it. A stagenet payout path is
a technical test surface, not authorization to offer real-money wagering.
`ALLOW_MAINNET_PAYOUTS=true` is an engineering interlock only. Attaching redeemable value requires
jurisdiction-specific legal review, age and access controls, responsible-play measures, and an
operational review that this document does not provide. This documentation is not legal advice.
Independent MIT deployments leave `OPERATED_PRODUCT_PROFILE` unset and are not constrained by any of
the product decisions above.
## Prerequisites
- A reviewed, tagged commit with the full test suite passing.
- A release artifact built and verified per [RELEASE_ARTIFACT.md](./RELEASE_ARTIFACT.md).
- A passing `npm run preflight` for each instance's environment file.
- A pre-deploy database backup for each instance (see [LOGS_AND_BACKUP.md](./LOGS_AND_BACKUP.md)).
## Operated product profile contracts
When `OPERATED_PRODUCT_PROFILE` is set, `validateOperatedProductProfile()` runs during preflight and
normal startup and rejects any network, identity, or economic-scope drift. Both profiles require:
| Setting | Required value |
|---------|----------------|
| `OPERATOR_NAME` | `Such Software` |
| `OPERATOR_CONTACT_URL` | `mailto:apps@such.software` |
| `OPERATOR_CONTACT_LABEL` | `apps@such.software` |
| `HOSTED_BY` | the profile's exact `https://` public root |
| `LEGAL_POLICY_VERSION`, `TERMS_EFFECTIVE_DATE` | the profile's exact values |
| `PAYMENTS_ENABLED`, `FREE_PLAY_ENABLED`, `PAID_ACKNOWLEDGEMENT_REQUIRED` | `true` |
| `SOLO_ENABLED`, `TAVERN_ENABLED`, `MATCH_ENABLED` | `true` |
| `MATCH_CRYPTO_RACE_ENABLED`, `MATCH_PAYOUTS_ENABLED` | `false` |
| `ALLOW_MAINNET_PAYOUTS` | `false` |
| products | no standalone cosmetic products, and no `grants` key on a credit package |
Per profile:
| Setting | `such-play-wow-prestige` | `such-monerogue-stagenet` |
|---------|--------------------------|---------------------------|
| `CRYPTO_TYPE` / `MONERO_NETWORK` | `WOW` / `mainnet` | `XMR` / `stagenet` |
| `GAME_MODE` | `PAID_CREDITS` | `PAID_SINGLE` |
| `PAYMENT_MODES` | `credits` | `direct` |
| `DIRECT_PAYMENT_ENABLED` | `false` | `true` |
| `CREDITS_ENABLED` | `true` | `false` |
| `PAYOUTS_ENABLED` | `false` | `true` |
| `DIRECT_PAYOUTS_ENABLED` | `false` | `true` |
| `CREDITS_PAYOUTS_ENABLED` | `false` | `false` |
| `DIRECT_PAYOUT_ESCAPE` / `DIRECT_PAYOUT_TREASURE` | not applicable | exactly `2.0` / `3.0` |
The profile also controls what `/api/game-modes` publishes: `selectPublicPaidModeDescriptors()`
emits only the profile's own paid mode, so a Wownero instance never advertises `PAID_SINGLE` and a
Monerogue instance never advertises `PAID_CREDITS`.
## Step 0: Stage an immutable release
Build one runtime artifact, then transfer only that blob into a root-owned mode-0700 candidate
inbox. Compare the host's SHA-256 against an independently recorded digest; the sidecar file that
travelled with the blob is not an independent provenance source.
Blob staging, two-dump clone validation ([CLONE_MIGRATIONS.md](./CLONE_MIGRATIONS.md)), wallet
candidate preparation and promotion, and per-instance release activation are performed by the
separate, default-closed, hash-pinned fleet automation. Each operation is bound to literal hashes
and exact receipts and infers no release, dump, wallet, predecessor, or instance. Manual extraction
under `/var/www/<instance>/releases`, manual dependency installation, manual service commands, and a
hand-written `current` symlink switch are not supported substitutes. A release directory is never
writable by either application service identity.
The artifact contains only `LICENSE`, `html/**`, and the runtime subset of `src/**`. Environment
examples, tests, documentation, the balance simulator, and root-level operator tooling and service
units are excluded by construction, so none of them can be provisioned out of a release directory.
## Step 1: Configure the mainnet instance
Install `src/.env.mainnet.example` as `/etc/wownerogue/app.env`, mode 0640, owner `root:wownerogue`,
and replace every `CHANGE_ME` value. Provision this file through the hash-pinned fleet or source
change, never from the application release directory.
The decisive money gates for this instance:
```ini
OPERATED_PRODUCT_PROFILE=such-play-wow-prestige
GAME_MODE=PAID_CREDITS # legacy/socket mode identity for the credits product
PAYMENT_MODES=credits
FREE_PLAY_ENABLED=true # free play is a choice; free runs rank on the Pleb board
CREDITS_ENABLED=true
DIRECT_PAYMENT_ENABLED=false # per-run payment is outside this product scope
PAYOUTS_ENABLED=false # no crypto leaves the house wallet
DIRECT_PAYOUTS_ENABLED=false
CREDITS_PAYOUTS_ENABLED=false
DIRECT_REQUIRES_ADDRESS=false # no payout means no payout address is collected
CREDITS_REQUIRES_ADDRESS=false
TRUST_PROXY=true
TRUST_PROXY_HOPS=1 # exactly one NPM hop
```
Amounts on this instance are WOW atomic units (1 WOW = 1e11).
Players buy non-redeemable credits for leaderboard and prestige play; free players use the Pleb
board. The product offers no winnings, payouts, or cash-out.
The Wownero wallet, its key file, password, systemd unit, and RPC credentials are outside the scope
of an application release. Changing any of them requires independently recoverable keyfile, password,
and mnemonic custody, an off-host recovery proof, an isolated rehearsal, exact address and balance
verification, and a separately reviewed maintenance and rollback plan. Root-level wallet templates in
`scripts/deploy/` are operations source, not runtime files and not an instruction to install them.
After activation, verify:
```bash
sudo systemctl restart wownerogue.service
journalctl -u wownerogue.service -n 50 --no-pager # payment system enabled, payouts disabled
curl -fsS localhost:3000/api/game-modes \
| jq -e '.FREE.enabled == true and .PAID_CREDITS.enabled == true and (has("PAID_SINGLE") | not)'
```
## Step 2: Prove the stagenet wallet boundary
Wallet work is a custody operation separate from application release work. It runs from a clean,
hash-pinned fleet revision against exactly one selected instance, and preserves every receipt.
Candidate preparation proves that the fixed encrypted stagenet snapshot reopens the exact live
address, has a spend key, and enforces strong Digest credentials, before it publishes inert
dedicated-user material. Promotion stops only the selected legacy wallet, installs only the reviewed
loopback-only unit, requires an unauthenticated `401` plus authenticated address, network, and
balance probes, and rolls back automatically to the exact legacy unit on a failed gate.
Neither operation reads, exports, or proves a mnemonic. A passing keyfile and password proof is not
independent seed custody and not an off-host recovery drill; record those separately until a
human-controlled custody inventory and an isolated off-host restore establish them. That gap is not a
reason to weaken a proof, print a secret, or apply the same workflow to the funded Wownero mainnet
wallet.
## Step 3: Create the monerogue identity, layout, and database
```bash
sudo useradd --system --home /var/www/monerogue --no-create-home --shell /usr/sbin/nologin monerogue
sudo install -d -m 0750 -o root -g monerogue /var/www/monerogue
sudo install -d -m 0750 -o root -g monerogue /var/www/monerogue/releases
sudo install -d -m 0750 -o root -g monerogue /etc/monerogue
sudo -u postgres createuser --pwprompt monerogue
sudo -u postgres createdb --owner=monerogue --template=template0 monerogue
```
The service identity owns its database role, not its application files. Do not clone source onto the
production host and do not recursively `chown` `/var/www/monerogue` to the service user.
## Step 4: Configure the monerogue instance
Install `src/.env.stagenet.example` as `/etc/monerogue/app.env`, mode 0640, owner `root:monerogue`.
Apply its non-secret policy keys through the fleet runtime-policy role; do not overwrite or log the
protected environment. Amounts are XMR atomic units (1 XMR = 1e12).
Host-specific values to set:
```ini
PORT=3001
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:38083 # monero-wallet-rpc, stagenet
PRIMARY_RPC_ENDPOINT=http://127.0.0.1:38081 # monerod, stagenet
FALLBACK_RPC_ENDPOINT=http://127.0.0.1:38081
DB_NAME=monerogue
DB_USER=monerogue
DB_PASSWORD=CHANGE_ME
ADMIN_API_KEY=GENERATE_WITH_openssl_rand_hex_32
SMIRK_ENABLED=false # Smirk does not support stagenet
```
Economy values, all in stagenet test coins with no real value:
```ini
DIRECT_GAME_PRICE=10000000000 # 0.01 XMR per run
DIRECT_PAYOUT_ESCAPE=2.0
DIRECT_PAYOUT_TREASURE=3.0
PAYOUT_MIN_AMOUNT=1000000000 # 0.001 XMR
PAYOUT_MAX_PER_GAME=50000000000 # 0.05 XMR cap; the reachable maximum is 3 x 0.01 = 0.03
PAYOUT_BATCH_INTERVAL=300
PAYOUT_MAX_RETRIES=3
BALANCE_WARN=100000000000 # warn below 0.10 XMR unlocked
BALANCE_CRITICAL=50000000000 # stop accepting new payout liability below 0.05 XMR
DIFFICULTY_PRESET=casino
```
Verify the unlocked stagenet bankroll before every rollout. Lowering `DIRECT_GAME_PRICE` to
`1000000000` (0.001 XMR) yields many more demo runs per faucet refill.
`CREDITS_PAYOUT_BASE`, `CREDITS_PAYOUT_ESCAPE`, `CREDITS_PAYOUT_TREASURE`, and `CREDITS_PACKAGES`
remain in the file as inert compatibility values. `CREDITS_ENABLED=false` and
`CREDITS_PAYOUTS_ENABLED=false` keep every credit purchase and credit payout path fail-closed, so
nothing exposes them.
The stagenet profile leaves `FINANCIAL_EVENT_SINK_URL` and `FINANCIAL_EVENT_SINK_TOKEN` blank.
Startup rejects a configured financial-event sink on a test network, because no-real-value events
must never reach a real accounting sink.
The service unit additionally requires `/etc/monerogue/wallet-rpc.env`, owner `root:monerogue`, mode
0640, containing only `WALLET_RPC_USER` and `WALLET_RPC_PASSWORD`. Provision it only through the
wallet-candidate workflow. Neither password belongs in a command argument, a unit `ExecStart`, shell
history, or this document.
Startup proves that wallet RPC actually enforces Digest authentication: a deliberate unauthenticated
`get_version` probe must receive a usable `401` Digest challenge before the authenticated identity
probe is accepted. Setting application credentials is not sufficient; a wallet RPC that still answers
the unauthenticated request fails readiness closed with `WALLET_RPC_AUTH_NOT_ENFORCED`.
```bash
sudo chmod 0640 /etc/monerogue/app.env
sudo chown root:monerogue /etc/monerogue/app.env
cd /var/www/monerogue/releases/<release-id>/src
sudo -u monerogue bash -c 'set -a; . /etc/monerogue/app.env; . /etc/monerogue/wallet-rpc.env; set +a; npm run preflight'
```
## Step 5: Activate the instance and close the network
The runtime artifact contains no service units. Wallet promotion and instance activation run through
the hash-pinned fleet roles after the recovery, clone-validation, protected-environment, wallet,
drain, and financial-audit gates pass. Do not copy a unit out of an application release.
Install the hash-pinned copies of `scripts/deploy/wowngeon-firewall.sh` (mode 0755 at
`/usr/local/sbin/wowngeon-firewall`) and `scripts/deploy/wowngeon-firewall.service`, then set the
fixed proxy address as `NPM_SOURCE_IPV4=<address>` in `/etc/wowngeon/firewall.env`. The script is
idempotent: it rejects any non-loopback traffic to ports 3000 and 3001 that does not come from that
address, and rejects all non-loopback traffic to the wallet and daemon JSON-RPC ports 34568, 34570,
38081, and 38083. Peer-to-peer ports are deliberately untouched.
Confirm afterwards that direct access to ports 3000 and 3001 is refused while both domains, their
websocket upgrades, and local readiness probes still work.
## Step 6: Verify end to end
- `monerogue.app` loads and `play.wowne.ro` stays healthy.
- Each public modes document contains only its own operated paid product: Wownero exposes
`PAID_CREDITS` and no `PAID_SINGLE`; Monerogue exposes `PAID_SINGLE` and no `PAID_CREDITS`.
- On Monerogue, run the bounded canary described in
[STAGENET_FINANCIAL_CANARY.md](./STAGENET_FINANCIAL_CANARY.md): pay entry from a stagenet wallet,
the game starts, a qualifying solo outcome pays 2x or 3x in test coins.
- Both boards populate on each instance: a free game ranks on Pleb, a paid game on the Hall of
Champions.
- On mainnet, confirm that purchased-credit play creates no payout or cash-out path.
## Rollback
- Stop only the affected instance, atomically repoint `/var/www/<instance>/current` at the previous
immutable release, and restart it.
- Keep the pre-deploy database backup for an operator-reviewed restore if a data rollback is needed.
The backup timer prunes dumps older than `BACKUP_RETENTION_DAYS` (default 14), so preserve a dump
you may still need outside the pruned directory.
- Preserve the previous release directory until the new one has passed its soak.