How to deploy discord-stfc-tools — a Cloudflare Worker Discord bot for STFC alliance verification, coordinate lookup, and tables.
- Discord admins (configure the bot in-server): see docs/ADMIN_GUIDE.md — nicknames, roles, personal channels, verification log, linking existing channels.
- Command list (single vs multi-alliance): docs/COMMANDS.md
- Architecture / development: see AGENTS.md.
| Component | Purpose |
|---|---|
Worker (src/index.ts) |
Slash commands, HTTP API, cron triggers |
Durable Object (DiscordGateway) |
Persistent Discord Gateway WebSocket (DM verification, member joins) |
D1 (STFC_DB) |
Guild config, verified players, stats history |
| R2 (optional) | Archived verification screenshots |
Workers Assets (public/) |
Static files only (index.html today) |
Slash commands: /lookup, /table, /tablehelp, /player, /verify, /server setup, /server status
Removed (Jul 2026): /officer and officer portrait assets — archived in archive/officers/.
- Node.js 18+ and npm
- Cloudflare account with Workers enabled
- Wrangler CLI (installed via
npm install) - Discord application at Discord Developer Portal
- Bot invited to your Discord server with permissions to:
- Manage roles
- Manage channels
- Manage nicknames
- Send messages (including DMs)
git clone <your-repo-url> discord-stfc-tools
cd discord-stfc-tools
npm install- Open Discord Developer Portal → New Application.
- Bot tab → Add Bot → copy the Bot Token (keep secret).
- General Information → copy Application ID.
- General Information → copy Public Key.
- Bot tab → Privileged Gateway Intents — enable:
- Server Members Intent
- Message Content Intent
- OAuth2 → URL Generator — scopes:
bot,applications.commands. Bot permissions: Manage Roles, Manage Channels, Manage Nicknames, Send Messages. Use the generated URL to invite the bot to your server.
npx wrangler d1 create stfc-dbCopy the database_id from the output.
cp .env.template .envEdit .env:
DISCORD_APPLICATION_ID=your-application-id
DISCORD_PUBLIC_KEY=your-public-key
DISCORD_BOT_TOKEN=your-bot-token
D1_DATABASE_NAME=stfc-db
D1_DATABASE_ID=your-d1-database-id-from-step-3
# Optional: override Worker script name (default stfc-tools). Set this if you
# already deployed under a different name — changing it creates a NEW worker.
# WORKER_NAME=stfc-tools
WORKER_URL=https://stfc-tools.your-subdomain.workers.devKV and R2 are optional (see Optional components below).
npx wrangler loginYour .env is the local source of truth. One command pushes secrets to Cloudflare and regenerates wrangler.json (vars + bindings):
npm run push-envThis script:
| What | Where it goes |
|---|---|
DISCORD_PUBLIC_KEY, DISCORD_BOT_TOKEN |
Encrypted Worker secrets (wrangler secret bulk) |
DISCORD_APPLICATION_ID, WORKER_URL |
Worker vars in wrangler.json |
WORKER_NAME |
Worker script name in wrangler.json (default stfc-tools) |
D1_DATABASE_*, KV_*, R2_* |
Bindings in wrangler.json |
| Same secrets | .dev.vars (for wrangler dev locally) |
Manual alternative (if you prefer not to use the script):
npx wrangler secret put DISCORD_PUBLIC_KEY
npx wrangler secret put DISCORD_BOT_TOKEN
npm run generate-configSee Pushing .env to Cloudflare for details on what goes where.
db:migrate applies pending files under migrations/ to remote D1 (tracked in _schema_migrations). That includes the base guild/player schema plus later migrations (surveys, urgent channel, excluded users, personal-channel permission template, alliance roster 024_alliance_roster.sql, etc.).
For local development only:
npm run db:migrate-localnpm run deployFirst deploy registers Durable Object migrations (DiscordGateway, StfcSession). This is automatic via wrangler.json / generate-config.js.
Note your Worker URL from the deploy output (e.g. https://stfc-tools.your-name.workers.dev). Update WORKER_URL in .env, then npm run push-env && npm run deploy again.
- Discord Developer Portal → your application → General Information.
- Set Interactions Endpoint URL to:
https://stfc-tools.your-subdomain.workers.dev/discord - Discord will send a verification ping — the Worker must already be deployed. Save when verification succeeds.
Not a separate webhook URL. Discord only has one application URL field for the bot:
| Discord portal field | Set to? | Purpose |
|---|---|---|
| Interactions Endpoint URL | https://…/discord |
Yes — required for slash commands (/verify, /server, etc.) |
| Webhook URL (elsewhere) | — | No — channel webhooks are unrelated; this bot does not use them |
| Gateway URL | — | No — Gateway connects outbound via the Durable Object automatically |
Member joins and DM verification use the Gateway (WebSocket), not a second URL in the portal. Ensure Server Members and Message Content intents are enabled on the Bot tab.
npm run register-commandsThis uses PUT to replace all global commands (removes any stale commands like /officer if they existed elsewhere).
In Discord, as an administrator, run /server setup then follow the full admin checklist in docs/ADMIN_GUIDE.md (nicknames, rank roles, personal channels, verification log, linking existing member channels).
Minimal example:
/server setup server:42 mode:single_alliance region:US alliance_tag:YOURTAG guest_role:123456789 member_roles:111,222
| Option | Description |
|---|---|
server |
STFC server number (required) |
mode |
single_alliance or multi_alliance |
region |
US or EU |
alliance_tag |
Required for single-alliance mode |
guest_role |
Role ID for unverified / wrong-alliance members |
member_roles |
Comma-separated role IDs granted on verification |
nickname_template |
Optional nick pattern (see admin guide) |
operative_roles … admiral_roles |
Optional rank roles |
Typical follow-ups:
/channels extra-roles roles:@Officer
/channels map category_map:A-F=…,G-M=…
/channels log create:true
/channels link channel:#existing-member player:PlayerName apply_permissions:false
/server status
Check configuration:
/server status
- Gateway:
curl https://your-worker.workers.dev/gateway/status— should show connection state after a minute. - Coordinates:
/lookup [[TAG] Player S:73559 X:628.7 Y:43.3]in Discord. - Verification: Have a test user join the server → they should receive a verification DM. They can reply with a profile screenshot, then their stfc.pro link, or use
/verify link:https://stfc.pro/....
You do not need someone to join to test verification. As a server administrator:
/server test-invite # sends verification DM to you (simulates join)
/server gateway # check Gateway WebSocket is connected
/verify link:https://stfc.pro/player/... # full verify flow in-channel
/server test-reset # clear your verification record and try again
Recommended test loop:
/server setup— configure guild once/server test-invite— bot DMs you with instructions- Reply in DM with screenshot → stfc.pro link or run
/verifyin the server /server status— confirm config; check roles/nickname updated/server test-reset— wipe your record to re-test
Optional: /server test-invite user:@Someone to test another admin's DMs.
DM not arriving? User must allow DMs from server members (Discord privacy). Bot needs DISCORD_BOT_TOKEN secret on the Worker. Check /server gateway shows Ready: yes.
npm testUse this if you already run an older version of this bot (coordinate lookup, /officer, OFFICERS_DB, KV migration, etc.).
| Before | After |
|---|---|
OFFICERS_DB binding |
STFC_DB binding (same Cloudflare D1 database) |
/officer command |
Removed — code in archive/officers/ |
public/officers/, public/abilities/ |
Removed from deploy (~14 MB) |
| Webhook-only bot | Webhook + Gateway Durable Object (DMs, member joins) |
| No verification | /verify, /server, /player, guild tables in D1 |
DISCORD_PUBLIC_KEY only |
Also requires DISCORD_BOT_TOKEN secret |
| No cron | Four cron triggers (member poll, guest re-check, daily roster+sync, demotion recheck) |
| KV for systems (documented) | Still optional; lookup uses bundled systemData.ts |
Your existing D1 database (stfc-officers or custom name) is reused. Legacy officer tables (officers, officer_translations, etc.) are harmless if left in place — the bot no longer queries them.
git pull
npm installAdd any missing variables (compare with .env.template):
DISCORD_APPLICATION_ID=... # already had this
DISCORD_PUBLIC_KEY=... # add if only set as Cloudflare secret before
DISCORD_BOT_TOKEN=... # required for command registration AND runtime
D1_DATABASE_NAME=stfc-officers # keep your existing database name
D1_DATABASE_ID=your-existing-id # keep your existing ID
# WORKER_NAME=stfc-tools # set if your deployed Worker name differs from default
WORKER_URL=https://stfc-tools.your-subdomain.workers.dev # your deployed URLDiscord Developer Portal → Bot → enable:
- Server Members Intent
- Message Content Intent
Without these, Gateway DM verification and GUILD_MEMBER_ADD will not work.
npm run push-env
npm run db:migrateThis uploads secrets and regenerates wrangler.json. If you already had secrets set manually on Cloudflare, this overwrites them with values from .env.
npm run deployFirst deploy after this upgrade creates the DiscordGateway Durable Object class. Watch deploy output for migration errors.
Important: Only one Gateway connection per bot token. Do not run wrangler dev and production simultaneously with the same token.
npm run register-commandsThis removes /officer from Discord and registers the new commands (/verify, /server, /player, etc.).
Confirm Interactions Endpoint URL still points to:
https://your-worker.workers.dev/discord
Discord may re-verify after deploy — check the Developer Portal shows a green checkmark.
Existing servers have no guild config until you run setup:
/server setup server:42 mode:single_alliance region:US alliance_tag:YOURTAG guest_role:... member_roles:...
curl https://your-worker.workers.dev/gateway/status| Test | Expected |
|---|---|
/lookup |
Still works (bundled system data) |
/table |
Still works |
/officer |
Gone — command should not appear in Discord |
| New member joins | Verification DM within ~5 minutes |
| DM screenshot + stfc.pro link | Roles assigned per /server setup |
/verify |
Still works as fallback |
These are not required for the new bot to work:
| Item | Action |
|---|---|
| Officer tables in D1 | Leave or drop manually (officers, officer_abilities, etc.) |
KV SYSTEM_DATA namespace |
Unused by runtime; can decommission |
archive/officers/ |
Keep in repo for reference or delete locally |
Old docs (KV_MIGRATION_GUIDE.md, etc.) |
Historical only |
To drop officer tables from D1 (irreversible):
npx wrangler d1 execute stfc-officers --remote --command \
"DROP TABLE IF EXISTS ability_values; DROP TABLE IF EXISTS officer_abilities; ..."Only do this if you are certain you will not revive officer lookup from archive/officers/.
Without R2, screenshot Discord URLs are stored in D1 but not permanently archived.
npx wrangler r2 bucket create stfc-verification-assetsAdd to .env:
R2_BUCKET_NAME=stfc-verification-assetsThen npm run push-env && npm run deploy.
Coordinate lookup reads from bundled src/systemData.ts. KV setup (npm run kv:create, migrate-kv, kv:upload) is optional and does not affect current behaviour. See KV_MIGRATION_GUIDE.md for historical context.
npm run dev-
Worker:
http://localhost:8787 -
Discord interactions require a public URL (use
wrangler dev --remoteor deploy to a preview worker for webhook testing). -
Gateway in local dev will compete with production if using the same bot token — use a separate Discord test application for local Gateway work.
-
Gateway in local dev will compete with production if using the same bot token — use a separate Discord test application for local Gateway work.
-
npm run push-envwrites.dev.varssowrangler devpicks up secrets automatically.
Cloudflare Workers split configuration into three layers:
| Layer | Examples | How to set |
|---|---|---|
| Secrets | DISCORD_PUBLIC_KEY, DISCORD_BOT_TOKEN |
Encrypted; npm run push-env or wrangler secret put |
| Vars | DISCORD_APPLICATION_ID, WORKER_URL |
Plaintext in wrangler.json; generate-config from .env |
| Bindings | D1, KV, R2, Durable Objects | wrangler.json; generate-config from .env |
.env is only on your machine (gitignored). Production does not read it automatically — you push values explicitly:
npm run push-env # secrets → Cloudflare, vars/bindings → wrangler.json
npm run deploy # apply wrangler.json to productionAfter changing any value in .env:
npm run push-env && npm run deployregister-commands reads DISCORD_* directly from .env (local only) — no push needed for that script.
- Validates
DISCORD_PUBLIC_KEYandDISCORD_BOT_TOKENexist in.env - Writes
.dev.vars(fornpm run dev) - Runs
generate-config→ updateswrangler.json - Runs
wrangler secret bulkwith those two secrets
- Never commit
.envor.dev.vars - Secrets in
.envare convenient for development; production copies are encrypted on Cloudflare - Rotating a token: update
.env, thennpm run push-env
| Variable | Required | Description |
|---|---|---|
DISCORD_APPLICATION_ID |
Yes | Vars in wrangler.json; also used by register-commands |
DISCORD_PUBLIC_KEY |
Yes | Worker secret (via push-env) |
DISCORD_BOT_TOKEN |
Yes | Worker secret + register-commands |
D1_DATABASE_NAME |
Yes (fresh) | Wrangler D1 database name |
D1_DATABASE_ID |
Yes (fresh) | Wrangler D1 database UUID |
WORKER_NAME |
No | Cloudflare Worker script name (default stfc-tools). Keep stable once deployed |
WORKER_URL |
Recommended | Deployed Worker URL |
KV_NAMESPACE_ID |
No | Optional KV binding |
KV_NAMESPACE_PREVIEW_ID |
No | Optional KV preview binding |
R2_BUCKET_NAME |
No | Screenshot archive bucket |
| Secret | Required | Description |
|---|---|---|
DISCORD_PUBLIC_KEY |
Yes | Verifies interaction signatures |
DISCORD_BOT_TOKEN |
Yes | REST API (roles, DMs, channels) + Gateway |
| Var | Purpose |
|---|---|
DISCORD_APPLICATION_ID |
Deferred interaction follow-ups |
WORKER_URL |
Public Worker URL |
ENVIRONMENT |
development (default) |
| Binding | Type | Purpose |
|---|---|---|
STFC_DB |
D1 | Guild and player state |
DISCORD_GATEWAY |
Durable Object | Discord Gateway WebSocket |
STFC_SESSION |
Durable Object | Anonymous stfc.pro session / token cache |
VERIFICATION_ASSETS |
R2 | Optional screenshot storage |
SYSTEM_DATA |
KV | Optional; unused at runtime |
Configured in generate-config.js → wrangler.json:
| Schedule | Purpose |
|---|---|
*/5 * * * * |
Wake Gateway; member poll fallback |
0 */6 * * * |
Re-check guest players (alliance roster cache first, else live lookup) |
0 6 * * * |
Alliance roster sync + day-over-day audit report + daily player sync |
30 * * * * |
Leave-detection recheck queue (auto policy missing-player delay) |
Single-alliance morning job: one HTML fetch of https://stfc.pro/alliances/{stfc_alliance_id} (full roster embedded in the page). Diff vs previous D1 snapshot → post to audit channel → sync verified players from cache. See docs/ADMIN_GUIDE.md § Daily alliance roster and AGENTS.md § Alliance roster sync.
Multi-alliance: no alliance roster sync; daily sync uses per-player lookups only.
| Path | Purpose |
|---|---|
GET /alliance-roster/ping |
Scrape alliance HTML (?alliance_id=&server=®ion=) |
GET /alliance-roster/ping?persist=1&guild_id= |
Scrape + write D1 roster for a single-alliance guild (returns diff counts) |
GET /stfc-session/ping |
HTML player lookup smoke test |
GET /gateway/status |
Discord Gateway DO status |
- Worker must be deployed before saving the URL in Discord.
DISCORD_PUBLIC_KEYsecret must match the application's Public Key exactly.
- Run
npm run register-commandsafter deploy. - Confirm interactions URL is correct and verified.
- Check Worker logs in Cloudflare dashboard.
DISCORD_BOT_TOKENsecret must be set on the Worker (not just in.env).- Privileged intents enabled in Discord portal.
- Run
/server setupon the guild. - Check
GET /gateway/status— Gateway should connect within a few minutes of deploy. - Users must allow DMs from server members (Discord privacy setting).
- Only one Gateway per bot token — stop local
wrangler devif production is running. - Check Cloudflare Worker logs for
DiscordGatewayerrors.
- Confirm
D1_DATABASE_IDin.envmatches your database. - Run
npx wrangler d1 listto verify database exists and you are logged into the correct account.
- Admin must run
/server setupwith the STFC server number and region.
# One-time / occasional
cp .env.template .env # fill in all values
npm install
npx wrangler login
npm run push-env # secrets → Cloudflare, wrangler.json from .env
npm run db:migrate
npm run deploy
npm run register-commands
# After .env changes
npm run push-env && npm run deploy
# Development
npm run dev # uses .dev.vars for secrets
npm test| File | Contents |
|---|---|
| AGENTS.md | Architecture, roadmap, coding guidelines |
| ENVIRONMENT_SETUP.md | .env and generate-config.js overview |
| FACTION_MAPPING.md | System faction IDs for /lookup |
| archive/officers/README.md | Removed officer feature |