Discord-first FPS queue bot with a live mobile viewer for sharing into WeChat or any mobile chat. Discord is the only control surface. The public viewer is mobile-friendly, backed by the same PostgreSQL state as the bot, and can now accept WeChat-authenticated joiners.
- Discord users create and manage queues.
- The backend is the single source of truth.
- Each queue gets a stable public share URL.
- The share URL opens a live viewer page that polls the backend for updates.
- The share card shown in chat does not mutate in place. The opened page stays current.
- The current implementation is WeChat-browser friendly and Mini Program ready, but it is not a WeChat chat bot.
- WeChat users can join a queue from the shared link after WeChat OAuth.
- Node.js 20+
- TypeScript
- Fastify
- discord.js
- PostgreSQL
- Prisma ORM
- Docker Compose
- Dynamic OG image rendering with
@resvg/resvg-js
m!queue 5m!queue 10m!join PARTYCODEm!add @USERm!leavem!updatecode NEWCODE
Discord queue messages also include built-in Join Queue and Leave Queue buttons.
Queue creation is intentionally two-step:
- Send
m!queue 5orm!queue 10 - Send the party code as your next plain text message
The queue is only created after the party code passes validation and uniqueness checks.
m!queue 5creates 5 main slots and 2 waitlist slots.m!queue 10creates 10 main slots and 2 waitlist slots.- Host auto-joins main slot 1.
- Party codes are unique among active queues.
- Each Discord user can only be in one active queue at a time.
m!joinfills main slots first, then waitlist.m!add @USERlets the current host add one mentioned Discord member into their active queue.m!leaveremoves the caller from their active queue.- Waitlist users auto-promote when a main slot opens.
- If the queue becomes empty, it is soft-deleted.
- If the host leaves while users remain, host transfers to the first occupied main slot.
m!updatecodeis host-only.- Active queues expire automatically after a configurable inactivity timeout.
- Queue 5 uses one main-slot list and one waitlist list.
- Queue 10 uses two columns for slots 1-5 and 6-10.
- Slot rows use boxed labels like
[01]and explicit status icons for occupied vs open slots. - Queue embeds include a shareable live viewer URL.
- The Discord message is edited in place after queue changes.
- Each queue message includes
Join QueueandLeave Queuebuttons. - Queue messages can include a
Sharebutton whenPUBLIC_BASE_URLis configured.
- Each queue has a stable share route:
/q/:shareSlug - The viewer is mobile-first and suitable for WeChat’s in-app browser.
- It shows live queue state from the backend.
- It polls every few seconds for updates.
- It includes dynamic metadata and an OG image endpoint for richer link previews.
- It includes a WeChat-friendly share action that hands out a WeChat join link.
src/
app/
config/
core/
db/
http/
platforms/
discord/
shared/
renderers/
services/
src/coreQueue types, validation, share payload helpers, and pure queue-state helpers.src/servicesQueue business logic, pending queue creation flow, share payload generation, and message sync.src/platforms/discordDiscord intake and message publishing.src/httpHealth route, public JSON API, share metadata routes, OG image route, and public viewer page.src/renderersDiscord embed rendering, queue page HTML, and OG image rendering.src/dbPrisma client and queue-record mapping.
Business logic lives in src/services/queue-service.ts. Discord and the public viewer both consume the same backend state instead of owning queue state locally.
Schema lives in:
Tables:
UserIdentityQueueQueueSlotQueueMessageRef
Important queue fields:
partyCodeUser-facing mutable code used by Discord commands.activePartyCodeUnique only while the queue is active.shareSlugStable public viewer identifier so the share URL survivesm!updatecode.lastActivityAtUpdated whenever queue membership or party code changes and used by the inactivity timer.
GET /health
GET /api/queues/:partyCodeActive queue snapshot by party code.GET /api/share/:shareSlugQueue snapshot by stable public share slug.
GET /api/queues/:partyCode/shareReturns share metadata for the active queue.
GET /api/queues/:partyCode/og-imageGET /api/share/:shareSlug/og-image
GET /q/:shareSlugPublic live viewer page.GET /join/:shareSlugSmart join link for mobile shares. In WeChat, it redirects through WeChat OAuth and joins the WeChat identity to the queue. Outside WeChat, it can still fall back to Discord OAuth if configured.GET /auth/wechat/callbackWeChat OAuth callback for share-link joins.GET /queue/:partyCodeRedirects active party codes to the stable share route.GET /auth/discord/callbackOptional Discord OAuth callback for non-WeChat share-link joins.
GET /internal/queues/:partyCode
If INTERNAL_API_TOKEN is set, send it as x-internal-token.
The public queue snapshot includes:
partyCodeshareSlugtypehostcurrentMainCountmaxMainCountwaitlistCountmaxWaitlistCounttotalCountavailabilitystatuslastActivityAtupdatedAtslotswaitlist
npm installcp .env.example .envPowerShell:
Copy-Item .env.example .envAt minimum:
DATABASE_URL=postgresql://queuebot:queuebot@localhost:5432/queuebot?schema=public
APP_PUBLIC_PORT=3000
DISCORD_ENABLED=true
DISCORD_CLIENT_ID=YOUR_CLIENT_ID
DISCORD_TOKEN=YOUR_ROTATED_BOT_TOKEN
PUBLIC_BASE_URL=http://localhost:3000
WECHAT_ENABLED=true
WECHAT_APP_ID=YOUR_WECHAT_OFFICIAL_ACCOUNT_APP_ID
WECHAT_APP_SECRET=YOUR_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET
WECHAT_OAUTH_SCOPE=snsapi_userinfo
QUEUE_BRAND_NAME=Valorant QueuePUBLIC_BASE_URL, WECHAT_APP_ID, and WECHAT_APP_SECRET are required for WeChat share-link joins. If you also want non-WeChat browsers to fall back to Discord OAuth, keep DISCORD_CLIENT_SECRET and the Discord redirect URI configured too.
docker compose up -d postgresnpx prisma migrate deployFor local schema iteration:
npx prisma migrate devnpm run dev- Health:
http://localhost:3000/health - Public viewer example:
http://localhost:3000/q/<shareSlug>
Bring up app and database together:
docker compose up --buildThe app container runs prisma migrate deploy before booting the server.
- Create a Discord application in the Discord Developer Portal.
- Create a bot user.
- Enable
MESSAGE CONTENT INTENT. - Copy the application client ID into
DISCORD_CLIENT_ID. - Generate a client secret and copy it into
DISCORD_CLIENT_SECRETonly if you want non-WeChat browsers to fall back to Discord OAuth. - In the OAuth2 settings, add this redirect URI:
https://YOUR_PUBLIC_DOMAIN/auth/discord/callback
- Set
DISCORD_SERVER_INVITE_URLto a permanent invite only if you want non-WeChat browsers to fall back into Discord after OAuth. - Copy the bot token into
DISCORD_TOKEN. - Invite the bot into your server with:
View ChannelsSend MessagesRead Message HistoryEmbed Links
- Start the bot and test:
m!queue 5- send a party code
m!join CODEm!add @USER
Discord runtime files:
This join flow is built for WeChat Official Account webpage authorization inside the WeChat browser.
- Create or use a WeChat Official Account with webpage authorization enabled.
- In the WeChat Official Account platform, copy:
- App ID into
WECHAT_APP_ID - App Secret into
WECHAT_APP_SECRET
- App ID into
- In the Official Account settings, add your production domain to the OAuth authorized domain list.
- Set
PUBLIC_BASE_URLto that same public HTTPS domain. - The callback URI used by the app is:
https://YOUR_PUBLIC_DOMAIN/auth/wechat/callback
- Choose the scope:
snsapi_userinfoif you want WeChat nicknames in queue slotssnsapi_baseif you prefer quieter auth and can accept generic WeChat display names
- Restart the app after updating env vars.
WeChat runtime files:
DATABASE_URLDISCORD_TOKENDISCORD_CLIENT_ID
PUBLIC_BASE_URLMust be your public domain in production so the share URLs and OG image URLs are absolute.WECHAT_APP_IDRequired for WeChat OAuth join links.WECHAT_APP_SECRETRequired for WeChat OAuth join links.WECHAT_OAUTH_SCOPEUsesnsapi_userinfoif you want nickname-based display names. Usesnsapi_baseif you prefer quieter authorization and can tolerate generic WeChat display names.DISCORD_CLIENT_SECRETOnly required if you want Discord OAuth as a fallback outside WeChat.DISCORD_SERVER_INVITE_URLUsed when a non-WeChat visitor signs in with Discord but is not yet in your server.QUEUE_BRAND_NAMEUsed in viewer metadata and OG image rendering.
APP_PUBLIC_PORTHost port Docker exposes publicly. Use80on the VM if you want cleanhttp://IPlinks instead of:3000.DISCORD_AFTER_JOIN_URLFallback Discord destination when the queue message URL is unavailable.PORTLOG_LEVELINTERNAL_API_TOKENDISCORD_PREFIXQUEUE_CREATION_TIMEOUT_SECONDSQUEUE_INACTIVITY_TIMEOUT_MINUTESQUEUE_INACTIVITY_SWEEP_SECONDSPARTY_CODE_MIN_LENGTHPARTY_CODE_MAX_LENGTHVIEWER_POLL_INTERVAL_SECONDS
When a queue is created:
- The queue is stored in PostgreSQL.
- The queue gets a stable
shareSlug. - The Discord embed is rendered with the queue state and share URL.
- The public viewer at
/q/:shareSlugreads from the same backend.
When the host changes the party code:
- The party code changes.
- The stable share URL does not change.
- The public viewer continues to work.
- New shares use the same stable viewer entry point with fresh metadata.
This MVP is designed for sharing the queue viewer into WeChat, not for controlling queues from a WeChat chat bot.
- Rich link metadata on the public viewer route
- Dynamic OG image endpoint
- Mobile-friendly page design
- Live polling after the page opens
- WeChat OAuth join flow from the shared link
- Shared backend updates the Discord queue message immediately after a WeChat join
- Shared cards inside chat do not mutate after being sent
- There is no native WeChat chat bot flow
- There is no Mini Program yet
- The share card is the polished entry point
- The opened viewer is live
- WeChat users can authenticate in WeChat and secure a slot without controlling the queue from chat
- Re-sharing later can reflect fresher metadata
The current backend is structured so a Mini Program can be added later without rewriting queue logic.
Use these endpoints from a future Mini Program client:
GET /api/share/:shareSlugGET /api/queues/:partyCodeGET /api/queues/:partyCode/share
The queue service and database remain unchanged. The new frontend would simply consume the JSON API.
Recommended layout:
- One Linux VM
- Docker Compose for
app+postgres - One public domain routed to Fastify
- Discord bot, API, viewer page, and OG image endpoint all hosted from the same service
- Install Docker Engine and Docker Compose on the VM.
- Copy the repo to the VM.
- Create
.envfrom.env.example. - Set
PUBLIC_BASE_URLto your real domain, for example:
PUBLIC_BASE_URL=https://queue.yourdomain.com- Fill Discord credentials.
- Start the stack:
docker compose up -d --build- Put a reverse proxy in front of the app.
Proxy examples:
- Queue 5 uses a single main-slot column.
- Queue 10 uses two columns.
- The page shows:
- party code
- queue type
- host
- current count
- waitlist count
- live status
- last updated
- copy-code action
Viewer rendering files:
npm run devnpm run buildnpm run startnpm run prisma:generatenpm run db:migratenpm run db:deploynpm run db:pushnpm run db:studio
- Discord is the only control platform.
- The public viewer is read-only.
- Inactivity cleanup is time-based and configurable through env vars.
- There is no auth on the public viewer.
- WeChat/WeCom chat-bot control is intentionally out of scope.
npm installnpx prisma generatenpm run build