Design and implement engaging multiplayer experiences in Discord Activities, including instance management, participant tracking, and avatar rendering.
When a user clicks “Join Application”, they expect to enter the same session their friends are participating in — whether it is a shared drawing canvas, a board game, a collaborative playlist, or a first-person shooter. The shared state of that session is called an application instance.The Embedded App SDK lets your app communicate bidirectionally with the Discord client. The instanceId is the key that both your application and Discord use to identify which unique instance of an application is active.
The instanceId is available as soon as the SDK is constructed, before the ready payload is received:
import {DiscordSDK} from '@discord/embedded-app-sdk';const discordSdk = new DiscordSDK(clientId);// Available immediately, no need to await ready()const instanceId = discordSdk.instanceId;
Use instanceId as the key for saving and loading shared data. This ensures all users in the same instance have access to the same state.
Instance participants are the Discord users currently connected to the same application instance. You can fetch the current participants or subscribe to updates:
import {DiscordSDK, Events, type Types} from '@discord/embedded-app-sdk';const discordSdk = new DiscordSDK('...');await discordSdk.ready();// Fetch current participantsconst participants = await discordSdk.commands.getInstanceConnectedParticipants();// Subscribe to participant changesfunction updateParticipants(participants: Types.GetActivityInstanceConnectedParticipantsResponse) { // Update your game state or UI}discordSdk.subscribe(Events.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE, updateParticipants);// Unsubscribe when donediscordSdk.unsubscribe(Events.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE, updateParticipants);
// Use the user object returned from authenticateconst {user} = await discordSdk.commands.authenticate({ access_token: accessToken,});let avatarSrc = '';if (user.avatar) { avatarSrc = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=256`;} else { // Fall back to a default avatar based on user ID const defaultAvatarIndex = (BigInt(user.id) >> 22n) % 6n; avatarSrc = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarIndex}.png`;}const username = user.global_name ?? `${user.username}#${user.discriminator}`;
To display a user’s guild-specific avatar and nickname, request the guilds.members.read scope. Note that this only returns information for the current user. To display guild-specific data for all participants, retrieve and serve it from your application’s server.
A common pattern is to retrieve and cache guild-specific avatar and nickname data on your application’s server, then serve all participant data from there. This avoids making per-user API calls from the client.
Your Activity’s website is publicly accessible at <application_id>.discordsays.com. When loaded outside of Discord, the Discord RPC server is not present and the Activity will likely fail. However, a malicious client could theoretically mock the RPC protocol.
Your backend can use this to verify a client is in a valid instance before allowing participation in gameplay. The approach is flexible — you might gate specific features, or decline to serve the Activity HTML entirely for unverified sessions.
For additional security, Discord provides optional proxy authentication. When your embedded app makes requests through the Discord proxy, each request can include cryptographic headers proving the request’s authenticity:
X-Signature-Ed25519 — a cryptographic signature
X-Signature-Timestamp — a Unix timestamp
X-Discord-Proxy-Payload — a base64-encoded payload with user context