Skip to main content

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โ€‹

MethodPathPurpose
GET/discord/bot-inviteBot invite URL + required permissions list
GET/discord/:guildId/channelsList text and voice channels
GET/discord/:guildId/rolesList Discord roles
GET/discord/:guildId/activityChannel activity (messages + voice members)
SSE/discord/:guildId/voice-streamReal-time voice state events
GET/organizations/:id/discord/in-serverWhether the linked Discord user is already in the linked Discord server (bot guild.members.fetch)
GET/organizations/:id/discord/server-inviteBot-created member invite URL (session + linked Discord + guild member)
PATCH/organizations/:id/settingsSave 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 first discordOverviewChannelIds entry, then discordAnnouncementChannelId, in that order.
  • Caching: A successful invite URL is stored under guild.settings.discordServerInviteCache (per channel) to avoid rate limits. Changing discordInviteChannelId clears the cache (via organization settings merge).
  • Already in server: GET .../discord/in-server uses the bot to resolve whether the userโ€™s linked Discord ID is a member of discordServerId (Unknown Member โ†’ not in server). The guild overview hides the invite card when inDiscordServer is true. The invite endpoint also returns 409 with ALREADY_IN_DISCORD_SERVER if 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):

ToggleDefaultEffect
Enable Discord integrationonMaster switch โ€” when off, no Discord actions fire for this event
Create Discord scheduled eventonCreates a native Discord Scheduled Event on publish
Send Discord announcementonPosts 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.