Discord Integration
Guild overview displays real-time Discord activity when a guild has a linked Discord server.
Architectureโ
flowchart LR
B1[Browser] -->|"REST poll (30s)"| A1["GET /discord/:id/activity"]
A1 --> D1["Discord.js cache + API"]
B2[Browser] -->|SSE stream| A2["GET /discord/:id/voice-stream"]
A2 --> D2[Discord gateway events]
Text channels use 30-second REST polling to show recent messages. Voice channels use Server-Sent Events (SSE) for real-time member join/leave updates, with the initial REST fetch as a baseline.
API Endpointsโ
| Method | Path | Purpose |
|---|---|---|
| GET | /discord/bot-invite | Bot invite URL + required permissions list |
| GET | /discord/:guildId/channels | List text and voice channels |
| GET | /discord/:guildId/roles | List Discord roles |
| GET | /discord/:guildId/activity | Channel activity (messages + voice members) |
| SSE | /discord/:guildId/voice-stream | Real-time voice state events |
| GET | /organizations/:id/discord/in-server | Whether the linked Discord user is already in the linked Discord server (bot guild.members.fetch) |
| GET | /organizations/:id/discord/server-invite | Bot-created member invite URL (session + linked Discord + guild member) |
| PATCH | /organizations/:id/settings | Save discordOverviewChannelIds, discordInviteChannelId, etc. |
Activity Endpointโ
Accepts optional channelIds query parameter (comma-separated) to filter which channels are returned. When omitted, all channels are returned.
Voice Stream (SSE)โ
Emits VoiceStateEvent objects when users join, leave, or move between voice channels:
interface VoiceStateEvent {
guildId: string;
channelId: string | null;
channelName: string | null;
channelType: 'voice' | 'stage';
userId: string;
displayName: string;
avatarUrl: string | null;
action: 'join' | 'leave' | 'move';
}
The backend listens to Discord gateway voiceStateUpdate events via an RxJS Subject and filters per-guild for each SSE connection. The Subject is completed on module destroy.
Frontend Hooksโ
useDiscordVoiceStreamโ
Custom hook that subscribes to the SSE endpoint and merges real-time voice data with the REST baseline.
- On first SSE event for a channel, seeds from REST data then applies live updates
- Once SSE has tracked a channel, its member list becomes authoritative (replaces REST)
- Resets patches when REST data refreshes (React "store previous props" pattern)
- Cleans up EventSource on unmount or when server ID changes
Member server invites (guild overview)โ
For guilds with a linked Discord server, guild members who have linked Discord on their account can request a bot-created invite from the guild overview (โJoin Discordโ). The API is GET /organizations/:id/discord/server-invite (authenticated).
- Channel selection: Optional
guild.settings.discordInviteChannelId(text or announcement channel). If unset, the invite is created on the firstdiscordOverviewChannelIdsentry, thendiscordAnnouncementChannelId, in that order. - Caching: A successful invite URL is stored under
guild.settings.discordServerInviteCache(per channel) to avoid rate limits. ChangingdiscordInviteChannelIdclears the cache (via organization settings merge). - Already in server:
GET .../discord/in-serveruses the bot to resolve whether the userโs linked Discord ID is a member ofdiscordServerId(Unknown Member โ not in server). The guild overview hides the invite card wheninDiscordServeris true. The invite endpoint also returns 409 withALREADY_IN_DISCORD_SERVERif someone bypasses the UI.
Configurable Overview Channelsโ
Guild admins can select which Discord channels appear on the overview tab via Settings > Integrations. Selected channel IDs are stored in guild.settings.discordOverviewChannelIds.
Event Announcementsโ
When events are published, the bot posts a rich embed to the configured Discord channel with:
- Event title, type, date, and description
- Signup link back to the platform
- Live roster that updates as users sign up (character name, class, spec)
- Instance/difficulty metadata when applicable
Per-Event Discord Controlsโ
The event form exposes a granular Discord section (only shown when the guild has a Discord server linked):
| Toggle | Default | Effect |
|---|---|---|
| Enable Discord integration | on | Master switch โ when off, no Discord actions fire for this event |
| Create Discord scheduled event | on | Creates a native Discord Scheduled Event on publish |
| Send Discord announcement | on | Posts a rich embed to the announcement channel on publish |
All three flags are stored in event.metadata (discordAnnounce, discordScheduledEvent). Absence of a flag is treated as true (defaults to guild behaviour). Setting a flag to false explicitly opts out.
Announcement Channel Overrideโ
When "Send Discord announcement" is on, an optional channel selector appears. Leaving it empty uses the guild default (guild.settings.discordAnnouncementChannelId). Selecting a channel stores it on event.discordChannelId and takes precedence over the guild default.
The announcement embed is a "living" message โ it updates automatically when signups change.
Voice Channel Overrideโ
When "Create Discord scheduled event" is on, an optional voice channel selector appears. This overrides the guild-level discordVoiceChannelId setting for this specific event. If no voice channel is chosen (event-level or guild-level), the scheduled event is created as an External type instead.
Voice channel override is stored in event.metadata.discordVoiceChannelId.
Discord Scheduled Eventsโ
Events can optionally create native Discord Scheduled Events, linked to voice channels when configured. See Event Lifecycle for details on auto-start, delay, and completion.
Voice Attendance Trackingโ
The bot tracks voice channel joins/leaves during active events. This data feeds into the attendance confirmation workflow. See Event Lifecycle for the full flow.
Bot Permissionsโ
Required Discord bot permissions:
- Manage Roles
- Manage Events
- Send Messages
- View Channels
- Read Message History
- Connect (voice)
- View Voice Channel Members
- Create Instant Invite (for member invite links on the guild overview)
Reauthorizing an existing installโ
Discord does not retroactively grant new scopes to a bot that was invited with an older OAuth URL. If channels, roles, or rank-to-role sync fail with permission errors, a server admin should open the same bot invite link again (GET /api/v1/discord/bot-invite โ used by the app). Authorizing in Discord adds any missing permissions to the bot already in the server; removing the bot first is unnecessary.
Guild Settings โ Integrations shows an Update bot permissions (wording may vary) callout whenever a Discord server is linked, plus the current required permission list. Channel/role load errors in that page also surface the invite button.