Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions docs/MAINNET.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ therefore mostly configuration, one genuine code decision (which anchor), and a
set of checks. It is written as a runbook: do the steps in order, and do not
skip the verification at the end of each phase.

**Read this first:** the public-network guardrails in `apps/api/src/env.ts`
throw at boot rather than warn. A service that refuses to start is loud; one
that quietly settles into a sandbox anchor is not. If a guard fires, fix the
configuration — never relax the guard to get a green deploy.
**Read this first:** the public-network guardrails throw at boot rather than
warn. A service that refuses to start is loud; one that quietly settles into a
sandbox anchor is not. The guards are split across two files: the OFFRAMP and
anchor-URL checks fire in `apps/api/src/env.ts` at module load (lines
114–142), as does the USDC issuer check (line 189); the
`DEFAULT_SELLER_WALLET` (line 386),
`SERVER_SIGNING_SECRET` (line 489), and `JWT_SECRET` (line 512) checks fire in
`apps/api/src/services/container.ts` inside `createContainer()`. If a guard
fires, fix the configuration — never relax the guard to get a green deploy.

---

Expand All @@ -30,7 +35,11 @@ configuration — never relax the guard to get a green deploy.
| Missing `JWT_SECRET` | Every seller is logged out on each deploy. |
| A blank or non-numeric numeric var | A blank value used to yield `0` — `TRUST_PROXY_HOPS=` silently collapsed every client into one rate-limit bucket. A typo yields `NaN`, and `setInterval(NaN)` is a tight loop against Horizon, not a slow poll. |

These are covered by `apps/api/test/env-mainnet-guards.test.ts`.
The OFFRAMP, USDC issuer, anchor-URL, and KYC key guards are covered by
`apps/api/test/env-mainnet-guards.test.ts`. The `DEFAULT_SELLER_WALLET`,
`SERVER_SIGNING_SECRET`, and `JWT_SECRET` guards live in
`apps/api/src/services/container.ts` and are not yet covered by a dedicated
test file.

---

Expand Down
10 changes: 7 additions & 3 deletions docs/RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ default.
| `DATABASE_URL` · `DATABASE_AUTH_TOKEN` | always (prod) | No persistence; falls back to a local SQLite file inside the container, which is destroyed on every deploy |
| `KYC_ENCRYPTION_KEY` | `OFFRAMP=testanchor` | **Process will not boot.** `env.ts` resolves it with `req()` at module load and throws `Missing required env var: KYC_ENCRYPTION_KEY` |
| `WEBHOOK_SECRET_ENCRYPTION_KEY` | `NODE_ENV=production` | **Process will not boot** (`createContainer()` calls `assertKeyConfigured()`). Before that check existed, it fell back to a hardcoded public dev key and 500'd on the first webhook registration |
| `JWT_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | Auto-generated per boot, so every restart and deploy logs every seller out |
| `SERVER_SIGNING_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | Auto-generated per boot, so the `SIGNING_KEY` published in `stellar.toml` changes on every restart and any wallet that cached it breaks |
| `JWT_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | **Process will not boot on public network** (`resolveJwtSecret()` in `apps/api/src/services/container.ts:512` throws). On testnet: auto-generated per boot, so every restart and deploy logs every seller out |
| `SERVER_SIGNING_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | **Process will not boot on public network** (`resolveServerSigningKeypair()` in `apps/api/src/services/container.ts:489` throws). On testnet: auto-generated per boot, so the `SIGNING_KEY` published in `stellar.toml` changes on every restart and any wallet that cached it breaks |
| `DEFAULT_SELLER_WALLET` | `STELLAR_NETWORK=public` | **Process will not boot on public network** (`resolveSellerKeypairOrWallet()` in `apps/api/src/services/container.ts:386` throws). On testnet: auto-generates a throwaway keypair and prints it (funds land in a key nobody kept across a restart) |
| `HOME_DOMAIN` | any real deployment | Falls back to `localhost:8787`. SEP-10 challenges are issued for localhost and `stellar.toml` advertises `WEB_AUTH_ENDPOINT="https://localhost:8787/auth"` — **wallet login cannot work at all** |
| `CORS_ORIGINS` | always | The browser refuses the dashboard's cross-origin calls |
| `DEFAULT_SELLER_SECRET` | `OFFRAMP=testanchor` with `DEFAULT_SELLER_WALLET` set | SEP-10 cannot sign the anchor's auth challenge, so every cash-out fails |
Expand All @@ -50,7 +51,10 @@ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
`render.yaml` declares all of these; the `sync: false` entries must be filled in
from the Render dashboard on first deploy. Adding a new `req()` call to
`apps/api/src/env.ts` without adding the matching `render.yaml` entry is what
caused the 2026-07-31 outage — see `docs/FIXLOG.md` `BUG-4.11`.
caused the 2026-07-31 outage — see `docs/FIXLOG.md` `BUG-4.11`. The same
mistake is possible in `apps/api/src/services/container.ts`, where
`DEFAULT_SELLER_WALLET` (line 386), `SERVER_SIGNING_SECRET` (line 489), and
`JWT_SECRET` (line 512) are also enforced at boot on public network.

### Scaling past one instance

Expand Down