diff --git a/backend/DEPLOY.md b/backend/DEPLOY.md new file mode 100644 index 00000000..1698e15d --- /dev/null +++ b/backend/DEPLOY.md @@ -0,0 +1,89 @@ +# Backend (indexer) — Deploy Runbook + +The backend is a runnable Node service (`src/index.js`) that indexes chain +transfers into a **stateful** local sqlite DB and serves a small read API. It is +built into a SHA-tagged, cosign-signed GHCR image and run as a Docker container +on the backend host by the `build-image-backend` + `deploy-backend` jobs in +`.github/workflows/quality-gate.yml`. + +> The KYC/council service (`src/server.js`) is a separate surface (Supabase + +> sudo signer). It is not containerized by this pipeline; only the indexer is. + +## Pipeline shape (mirrors the web deploy) + +``` +backend (npm ci + npm test + audit) + └─ telegram-gate (CEO approves in Telegram — SAME gate as web) + └─ build-image-backend (docker build ./backend → GHCR : + :latest, cosign sign) + └─ deploy-backend (cosign verify → ssh host → docker run → /health → auto-rollback) +``` + +- **Trigger:** only `main` or tags, `push`/`workflow_dispatch`. Never fork PRs + (the `if:` requires `refs/heads/main`|`refs/tags/*` + `packages:write`, which + fork PRs never get). +- **Rollback:** `gh workflow run quality-gate.yml -f rollback_to=` — skips + build, re-runs the old signed image. The deploy step also **auto-rolls-back** + to the previously-live SHA on a failed health check. + +## STATEFUL DB — do not wipe (critical) + +The indexer's sqlite file **is** the service's memory. It lives in the Docker +**named volume `pwap-indexer-db`**, mounted at `/data`, with +`DB_PATH=/data/transactions.db`. + +- The image never contains the DB (see `.dockerignore` + `Dockerfile` `VOLUME`). +- `deploy-backend` does `docker rm -f` + `docker run` on every deploy/rollback + but **never touches the volume** — state survives every deploy and rollback. +- Never run `docker volume rm pwap-indexer-db` as part of a deploy. Back it up + before host maintenance: + `docker run --rm -v pwap-indexer-db:/data -v "$PWD:/backup" busybox tar czf /backup/indexer-db.tgz -C /data .` + +## One-time host setup + +On the backend host (as the `BACKEND_VPS_USER`): + +```bash +mkdir -p /opt/pwap-indexer +# Runtime env for the container (owner-managed, NOT in git): +cat > /opt/pwap-indexer/.env <<'EOF' +WS_ENDPOINT=wss://rpc.pezkuwichain.io +PORT=3001 +# DB_PATH is set by the deploy to /data/transactions.db — leave unset here. +EOF +``` + +Docker must be installed and the deploy user able to run it. Port `3001` is +published on the host; front it with the existing nginx/Cloudflare if it needs +to be public (remember `set_real_ip_from` behind the CF proxy). + +## Required GitHub secrets + +New (backend-specific): + +| Secret | Purpose | +| --- | --- | +| `BACKEND_VPS_HOST` | Backend host address (no hardcoded IP in the workflow). **If the backend shares the web DEV VPS, set this equal to `VPS_HOST`.** | +| `BACKEND_VPS_USER` | SSH user on the backend host. | +| `BACKEND_VPS_SSH_KEY` | SSH private key for that user. | +| `BACKEND_VPS_SSH_PORT` | Optional; defaults to `22`. | + +Reused from the existing web pipeline (no new value needed): +`GITHUB_TOKEN` (GHCR push/pull + cosign), `PEXSEC_BOT_TOKEN`, +`TELEGRAM_CEO_CHAT_ID`. + +## Post-deploy smoke checks + +```bash +curl -fsS http://:3001/health # {"status":"ok"} +curl -fsS http://:3001/api/stats # {"total": } — grows as blocks index +docker inspect --format '{{.State.Health.Status}}' pwap-indexer # healthy +docker volume inspect pwap-indexer-db # confirm the DB volume exists/persists +``` + +## Notes / can't-run-in-this-environment + +- **cosign keyless signing/verification** only works inside GitHub Actions + (needs the Sigstore OIDC token). It cannot be exercised locally/offline. +- The offline test suite (`npm test`, `test/*.test.js`) needs no chain/DB. The + `integration-tests/*.live.test.js` suites need a live chain + Supabase and are + **excluded** from CI on purpose.