Discord Activities Backend (2026): Auth and Instance Security
Build a Discord Activity backend safely: OAuth code exchange, Activity Instance verification, signed proxy requests, URL Mappings and durable state.
A Discord Activity is a web application embedded inside Discord and connected to the host client through the Embedded App SDK. Discord gives the Activity useful context, OAuth, an Activity instance identifier, participant events and a privacy-preserving proxy, but that context is not automatically your game account, your database or your authoritative game state.
The security distinction matters more than the framework choice: an instanceId is a routing key, not proof that a request came from a legitimate Discord Activity. Discord explicitly warns that the public <application_id>.discordsays.com site can be loaded outside the intended client context and that RPC behavior can be mocked. For meaningful gameplay, the backend needs to verify Discord context rather than trusting identifiers copied out of the browser.
Architecture in one line: use Discord OAuth to establish user identity, verify the Activity instance or signed proxy request at the backend edge, map the Discord user to your own durable player id, and keep ephemeral instance state separate from durable progression.
1. What Discord gives you, and what it does not
During a running Activity, discordSdk.instanceId identifies the Activity instance. Users in the same instance share that id, and Discord exposes getInstanceConnectedParticipants() plus ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE for the current participant set. When the instance lifecycle ends, a later launch receives a different instance id.
| Concern | Discord provides | Your backend still owns |
|---|---|---|
| User authorization | Embedded App SDK OAuth flow | Token exchange, account mapping, game authorization |
| Activity session | instanceId + participant events | Verification, room/session state, recovery policy |
| Network privacy | Discord proxy + URL Mappings | API authorization, rate limits, request validation |
| Persistent progression | Not a game database | Profiles, saves, inventory, scores, entitlements |
| Realtime game simulation | Participant context, not gameplay replication | Your realtime service/framework if the game needs one |
2. OAuth: exchange the authorization code on the server
The Embedded App SDK can request an authorization code. Your Activity sends that short-lived code to your backend; the backend exchanges it with Discord using the application client secret. Do not ship the client secret in the Activity bundle.
// Activity client
const { code } = await discordSdk.commands.authorize({
client_id: APPLICATION_ID,
response_type: "code",
prompt: "none",
scope: ["identify"]
});
const tokenResponse = await fetch("/.proxy/api/oauth/discord", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code })
});
const { access_token } = await tokenResponse.json();
await discordSdk.commands.authenticate({ access_token });
The backend exchange establishes Discord identity; it should then resolve that identity to an internal player id. Do not use a client-supplied Discord user id as authorization just because it matches the UI. For a cross-platform game, the internal id is also where Discord, Steam, web or console identities can converge without duplicating progression.
3. Activity instance security: instanceId is not authentication
It is tempting to send instanceId to your server and treat possession of the id as proof that the caller belongs to that Activity. Do not. The identifier is useful for routing players to the same game session, but it is not a secret and it is not a server-verifiable credential by itself.
Discord documents an Activity Instance API for this boundary: the backend can query GET /api/applications/<application_id>/activity-instances/<instance_id> using the application Bot token. Verify the instance before admitting the caller to meaningful gameplay, then cache the successful verification for a short period appropriate to your session model rather than calling Discord on every frame or game input.
Admission rule: authenticated player + verified Discord Activity instance + your game/session authorization. None of those three is a substitute for the others.
4. Signed proxy requests are a second useful proof
Discord also supports proxy authentication. Requests traversing the Activity proxy can carry:
X-Signature-Ed25519X-Signature-TimestampX-Discord-Proxy-Payload
Your backend can verify the signature with the Discord application public key, validate the timestamp/expiry, and parse the proxy payload. This tells the backend that the request came through Discord's Activity proxy with a valid signature. It still does not decide whether that user is allowed to spend currency, submit a score or mutate another player's data; those remain game-authorization decisions.
If an endpoint can accept both Activity-proxied traffic and direct server-to-server traffic, make the two authentication modes explicit instead of silently treating a missing proxy signature as equivalent.
5. URL Mappings: route API and WebSocket traffic through the Activity proxy
Activities run behind Discord's proxy. Configure URL Mappings in the Developer Portal for external API and realtime hosts, then call the mapped Activity path rather than hard-coding the raw external origin into the browser bundle. Discord's current local-development guidance notes that the mapping target should be configured without the protocol; the mapping itself can be used for HTTPS/WSS traffic through the proxy.
// Browser-facing Activity path
fetch("/.proxy/api/player/profile");
// A realtime mapping can similarly route a mapped WSS path
const socket = new WebSocket("wss://" + location.host + "/.proxy/realtime");
Developing against a localhost override is convenient, but it bypasses pieces of the production proxy behavior. Before launch, test OAuth, API calls, WebSocket upgrades, CSP behavior and error handling inside the real Discord Activity proxy.
6. Realtime state: Discord provides the roster, not your game loop
getInstanceConnectedParticipants() is useful for presence. It is not an authoritative simulation or state-replication protocol. If two players can mutate shared state concurrently, decide which process owns that state.
- Turn-based or low-conflict Activity: a request/response API plus optimistic concurrency may be enough.
- Fast shared state: use a realtime room/server framework such as Colyseus or your own WebSocket service.
- Competitive simulation: keep decisive game logic on a trusted server process and send clients results/state, not authority.
Use the verified instanceId as one possible session-partition key, but do not make it the permanent player key. If the Activity closes and is relaunched, durable progression must still resolve from the internal player identity.
7. Persistent state: instance data and player data have different lifetimes
Keep ephemeral session state and durable progression separate:
| Data | Good key | Lifetime |
|---|---|---|
| Current round / lobby board | Verified Activity instance or game-session id | Minutes / session |
| Player profile / unlocks | Internal player id | Months / years |
| Leaderboard result | Internal player id + season/match id | Season / durable |
| Discord OAuth link | Provider subject ↔ internal player id | Until unlinked/revoked |
For shared durable writes, use optimistic concurrency or an authoritative server path. Do not let several clients overwrite one blob and hope last-writer-wins happens to be correct.
8. Where Crux fits, and where it does not
Crux can own the game-backend side of the boundary: player accounts, player documents/cloud save, shared project state, leaderboards, economy balances, live config and other durable API-backed state. It can be the system your Discord identity maps into.
Crux does not currently verify Discord Activity instances or Discord proxy signatures for you. It also does not turn the Embedded App SDK into a Discord-hosted realtime game loop. Your Activity/backend integration still has to perform the Discord OAuth/instance/proxy verification described above, then map the verified Discord identity to the appropriate Crux player/account flow.
If your Activity needs high-frequency realtime room simulation, pair the durable backend with an appropriate realtime framework/service instead of pushing every frame through a persistence API.
9. Pre-launch checklist
- Client secret exists only on trusted server infrastructure.
- OAuth code exchange validates errors and never trusts a client-supplied Discord id alone.
instanceIdis treated as a routing key, not proof.- Meaningful sessions verify the Activity instance through Discord's Activity Instance API and/or appropriately validate signed proxy traffic.
- Proxy signatures validate
X-Signature-Ed25519, timestamp/expiry andX-Discord-Proxy-Payloadwith the application public key. - API and WSS endpoints work through production URL Mappings, not only localhost overrides.
- Durable state is keyed to an internal player id, not the ephemeral Activity instance.
- Realtime authority is explicit for every contested piece of state.
- Discord/Crux/external-service outages have a defined retry or degraded-mode policy.
Related guides
Sources
Technical, pricing, and product claims were checked against these primary sources on the verification date above.