From 5d55ae67112a534ab5e589f39abdfab69d85e1ba Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Thu, 13 Aug 2026 20:04:27 -0400 Subject: [PATCH] docs(deploy): document Discord bot activation gate --- .env.example | 9 ++++++--- DEPLOYMENT.md | 24 +++++++++++++++++++++++- docs/bot/context.md | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index b4fd7d4..67488d0 100644 --- a/.env.example +++ b/.env.example @@ -38,9 +38,12 @@ POSTGRES_DB=ptf-app-dev POSTGRES_PORT=5432 APP_PORT=3000 -# --- Discord bot (disabled / not deployed yet) --- -# Kept here for documentation. The bot is excluded from the deployment image -# per DEPLOYMENT.md — these only apply when running the bot standalone. +# --- Discord bot (presence-gated) --- +# The bot ships inside the Docker image but starts ONLY when DISCORD_TOKEN is +# set in the environment (Coolify per-env gate — set it in prod only; leave +# unset in dev/stage). DISCORD_GUILD_ID is REQUIRED whenever the token is set +# (the bot config throws at import if it's missing). All other DISCORD_* vars +# are optional. # DISCORD_TOKEN= # DISCORD_GUILD_ID= # DISCORD_OPS_CHANNEL_ID= diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 57b8170..a040b93 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -29,7 +29,7 @@ Local full-stack dev with the same image shape is described at the end (`docker - **PostgreSQL** is provisioned by Coolify as a *separate service* in each environment (`dev`, `stg`, `prd`). The app talks to it via `DATABASE_URI` injected by Coolify. - **Payload uploads** (`media/` collection) are written to a Coolify **Persistent Storage Volume** mounted at `/app/media` in the app container, so uploads survive container restarts, rollbacks, and rebuilds. - **Scheduled one-shot jobs** (game-tick, market-tick) run via `docker exec` Coolify Scheduled Tasks against the running app container — same image, same env, no second service required. -- **The Discord bot is NOT deployed** today (`src/bot/` is planned but not started). The image still contains the source for future use; no bot service is wired in this guide. +- **The Discord bot ships inside the image** (`src/bot/`) and activates per-environment when `DISCORD_TOKEN` is set — prod only. It shares the same Postgres DB via the container env and inherits the single-instance constraint: **2 replicas = two bot instances** (duplicate command registration, double embeds/DMs). Notifications generated while the container is down are **not backfilled** (the Discord notification bridge polls with an in-memory cursor). > ⚠️ **Single-instance only.** The realtime SSE pipeline (`gameTick → /api/game-tick/notify → in-process bus → SSE /api/realtime → client router.refresh()`) uses an **in-memory subscriber bus** (`src/lib/realtime/bus.ts`) that is not shared across processes. Coolify *must* run **exactly 1 replica** of the app container per environment for SSE to work. If you scale the app horizontally, SSE stops working (live shipment/event log updates silently fail). Everything else continues to work; you'll need a distributed bus before you can scale. See `src/lib/realtime/bus.ts` and `AGENTS.md` for context. @@ -89,9 +89,30 @@ The Dockerfile pins `bun@1.3.11` and `node:22-alpine`. If you want to bump eithe | `EMAIL_PORT` | — | `587` | | `EMAIL_USERNAME` | — | SMTP user | | `EMAIL_PASSWORD` | — | SMTP password / API key | +| `DISCORD_TOKEN` | iff bot enabled in this env | Set in **prod only** — presence activates the bot (activation gate below). | +| `DISCORD_GUILD_ID` | iff `DISCORD_TOKEN` set | Config throws at import if the token is set and this is missing. | +| `DISCORD_OPS_CHANNEL_ID` | — | Ops channel for attendance RSVP embeds. | +| `DISCORD_ANNOUNCE_CHANNEL_ID` | — | Channel for `/announce`. | +| `DISCORD_STAFF_ROLE_IDS` | — | Comma-separated staff role IDs (staff-only `/announce`). | +| `DISCORD_ATTENDANCE_POLL_MS` | — | Attendance reconcile poll interval; default `60000`. | +| `DISCORD_NOTIFICATION_POLL_MS` | — | Notification bridge poll interval; default `20000`. | > Never copy the dev `PAYLOAD_SECRET` into prod. Never copy `GAME_TICK_NOTIFY_SECRET`. Generate fresh per environment. +### Discord bot activation gate + +The bot ships inside the same app image and starts **only when `DISCORD_TOKEN` is present**: + +1. **Presence gate** — the container entrypoint checks for `DISCORD_TOKEN`; if it's unset the bot never starts and the web app is unaffected. +2. **Fail-fast on missing guild id** — if `DISCORD_TOKEN` is set but `DISCORD_GUILD_ID` is missing, `src/bot/config.ts` throws at import time and the container fails fast. Set both together. +3. **Prod preflight checklist** — configure `DISCORD_OPS_CHANNEL_ID` and `DISCORD_STAFF_ROLE_IDS` **before** adding the token: on startup the bot immediately posts attendance embeds and starts DMing users per their notification preferences. +4. **Same-token-across-envs rationale** — the token is ONE Discord application credential, not a per-env secret. The value is identical across environments only because the presence gate restricts activation to prod; this does **not** conflict with the "never copy secrets between envs" guidance above, which governs per-env secrets like `PAYLOAD_SECRET` / `GAME_TICK_NOTIFY_SECRET`. +5. **Session-flapping warning** — never run the bot with the prod token from two places at once (e.g. local dev + prod): Discord force-disconnects one of the sessions. +6. **Env-clone warning** — a Coolify "clone environment" copy would carry the token along. Remove it from non-prod copies, or dev/stage would silently activate the bot. +7. **Recovery** — the entrypoint does **not** supervise/restart the bot. If the bot crashes, the web app stays healthy (announced subordinate-uptime default); recovery = Coolify Restart or redeploy. Troubleshooting: "token set but no bot login" → check the container logs for the bot's fail-fast line and verify the token/guild are valid. + +> ⚠️ The bot shares the single-instance constraint (Sec. 1): **running 2 replicas = two bot instances** — duplicate command registration, double embeds/DMs. Notifications generated while the container is down are **not backfilled** (in-memory cursor). + --- ## 6. Scheduled jobs (game-tick, market-tick) @@ -242,6 +263,7 @@ docker compose exec app sh # shell inside the app container | Realtime updates stopped working | More than 1 replica running, OR scheduled tick stopped | Limit replicas to 1. Check the scheduled tasks in Coolify are firing and `GameEventLogs` show fresh `gameTick` events. | | First build OOMs | Host has < 8 GB RAM during Next build | The `bun run build` script sets `--max-old-space-size=8000`. Give the Coolify build host at least 8 GB, or lower this in `package.json#scripts.build` (memory will become the limiter earlier). | | `bun run db push` fails PostGIS error | This is expected (`must be owner of table spatial_ref_sys`) | Use `bun run payload migrate` only. See `AGENTS.md` Gotchas. | +| Token set but bot never logs in / silent bot death | Entrypoint gate, config throw, or revoked/invalid token | Check the container logs for the bot's fail-fast line. Confirm `DISCORD_GUILD_ID` is set (the config throws at import if missing) and the token is valid/not revoked. The web app is unaffected — the bot is an in-process subordinate. | --- diff --git a/docs/bot/context.md b/docs/bot/context.md index 4a4fdae..074e31f 100644 --- a/docs/bot/context.md +++ b/docs/bot/context.md @@ -1,6 +1,6 @@ # Discord Bot — Product Context -Status: **design phase, not started**. Requirements captured from Jason's explanation (2026-08-11). Keep this file updated as decisions are made — it is the canonical reference for what the bot must do. +Status: **implemented and shipping**. The bot runs inside the production Docker image and activates per-environment when `DISCORD_TOKEN` is set (prod only) — see `../../DEPLOYMENT.md` §5. Keep this file updated as decisions are made — it is the canonical reference for what the bot must do. Planned location: `src/bot/` (same repo as the web app — the bot runs as a standalone long-running process importing `@payload-config`, following the existing `src/scripts/` / `src/tools/seed/` pattern).