1
0
Fork 0

docs(deploy): document Discord bot activation gate

This commit is contained in:
Jason Fraley 2026-08-13 20:04:27 -04:00
parent 6c97b1a41c
commit 5d55ae6711
3 changed files with 30 additions and 5 deletions

View file

@ -38,9 +38,12 @@ POSTGRES_DB=ptf-app-dev
POSTGRES_PORT=5432 POSTGRES_PORT=5432
APP_PORT=3000 APP_PORT=3000
# --- Discord bot (disabled / not deployed yet) --- # --- Discord bot (presence-gated) ---
# Kept here for documentation. The bot is excluded from the deployment image # The bot ships inside the Docker image but starts ONLY when DISCORD_TOKEN is
# per DEPLOYMENT.md — these only apply when running the bot standalone. # 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_TOKEN=
# DISCORD_GUILD_ID= # DISCORD_GUILD_ID=
# DISCORD_OPS_CHANNEL_ID= # DISCORD_OPS_CHANNEL_ID=

View file

@ -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. - **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. - **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. - **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. > ⚠️ **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_PORT` | — | `587` |
| `EMAIL_USERNAME` | — | SMTP user | | `EMAIL_USERNAME` | — | SMTP user |
| `EMAIL_PASSWORD` | — | SMTP password / API key | | `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. > 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) ## 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. | | 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). | | 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. | | `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. |
--- ---

View file

@ -1,6 +1,6 @@
# Discord Bot — Product Context # 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). 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).