Backend for Discord Activities (2026): Auth, State, and Persistence for Embedded Games
What a backend has to do for a Discord Activity in 2026: bridge Discord OAuth2 to your own player identity, persist progression across sessions, sync real-time multiplayer state, and survive the discordsays.com proxy and CSP sandbox. Build-vs-buy with roll-your-own, Firebase, PlayFab, and Crux.
A Discord Activity is a web app that runs inside an iframe embedded directly in the Discord client. Players launch it from a voice channel, and everyone in that channel can join the same session without installing anything. Under the hood it is plain web tech (HTML, JavaScript, WebSockets) wired to Discord through the Embedded App SDK. The catch developers discover quickly is that the SDK gives you identity, presence, and a sandboxed network path, but almost nothing else. The moment you want accounts, saved progress, or authoritative multiplayer state, you need a backend.
This guide walks through what that backend actually has to do, the four jobs that matter most: bridging Discord OAuth to your own player identity, persisting data across sessions, synchronizing real-time multiplayer state, and dealing with the proxy and CSP sandbox that every Activity runs inside. Then we look at build-vs-buy, comparing rolling your own, Firebase, PlayFab, and Crux as options rather than a single recommendation.
Note: Discord Activities are distinct from bots. A bot runs against the gateway and reacts to messages. An Activity is a sandboxed web app loaded in an iframe, talking to the host client through the Embedded App SDK. The two can coexist, but the backend concerns here are about the Activity, not the bot.
1. What a Discord Activity Is and Why It Needs a Backend
When a user opens your Activity, Discord loads it from a sandboxed proxy domain in the shape https://{clientId}.discordsays.com. That proxy hides player IP addresses and blocks known malicious endpoints, which is good for safety but means your app does not get the open internet it expects. The SDK hands you a handful of useful primitives: the user's Discord identity (after an OAuth handshake), an instanceId that uniquely identifies the running session, and a list of connected participants.
What the SDK does not give you is anywhere to keep things. There is no Discord-hosted database for your scores, no place to store a player's unlocked items, and no built-in server that reconciles two clients fighting over the same game state. Discord supplies the instance identifier and participant tracking; you are expected to implement everything stateful yourself. That gap is exactly what a game backend fills.
2. The Auth Bridge: Discord OAuth to Your Player Identity
Authentication is the first thing your backend touches, and the flow is a server-side OAuth2 exchange split deliberately between client and server so your client secret never leaves your machine. The client kicks it off, your backend completes it. The sequence the Embedded App SDK expects looks like this:
// 1. Construct the SDK with your OAuth2 client id
const discordSdk = new DiscordSDK(YOUR_OAUTH2_CLIENT_ID);
// 2. Wait for the host client to be ready
await discordSdk.ready();
// 3. Pop the consent modal and get a short-lived authorization code
const { code } = await discordSdk.commands.authorize({
client_id: YOUR_OAUTH2_CLIENT_ID,
response_type: "code",
state: "",
prompt: "none",
scope: ["identify", "applications.commands"],
});
// 4. Exchange the code for an access token on YOUR backend
const res = await fetch("/.proxy/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code }),
});
const { access_token } = await res.json();
// 5. Hand the token back to the Discord client
const auth = await discordSdk.commands.authenticate({ access_token });
The important step for your backend is number four. The client sends the authorization code to your token endpoint. Your server then calls Discord's OAuth token endpoint using your client_id and client_secret to swap that code for an access token. The secret stays server-side, which is the whole point of the split. The token comes back, the client calls authenticate(), and now you know which Discord user this is.
Knowing the Discord user is necessary but not sufficient. Most games want their own player record, one that can hold progression, currency, friends, and entitlements independent of Discord. So your backend takes the verified Discord identity and maps it to an internal player id, creating a new account on first sight and looking up the existing one on return. This identity bridging is the same pattern you would use to attach any external login provider, and it is worth getting right early because retrofitting account merging later is painful. We cover the broader version of this problem in our guide on player authentication for dedicated server games.
Identity decisions your backend has to make:
- First-launch: create an internal player keyed to the Discord user id, or prompt to link an existing account.
- Returning player: resolve the Discord id to the existing internal player record.
- Cross-platform: if the same game also ships on Steam or web, you need one identity that several login providers can attach to, so a Discord player and a Steam player can be the same person.
If you want players to start instantly and only formalize their account later, a guest-then-upgrade flow pairs well with Discord launches. We dig into that pattern in guest login and account upgrade.
3. Persistent Data and Cross-Session State
An Activity instance is short-lived by design. The documentation defines the end of its lifecycle plainly: when all users of an application in a channel leave or close it, that instance has finished. The instanceId is a useful key for data scoped to a single live session, but it is gone the moment the session ends. Anything that should outlive the session, a player's level, their cosmetics, their match history, has to live in durable storage that your backend owns.
This is the difference between session state and persistent state, and conflating them is a common early mistake. Session state is "who is in this lobby right now and what is the current board." Persistent state is "this player has 1,240 coins and unlocked the third map." The first can be ephemeral and keyed by instanceId. The second must be keyed by your internal player id and written to a database that survives restarts, crashes, and the player closing Discord entirely.
Your persistence layer also has to handle concurrency honestly. If two devices for the same account are open, or a write races with a read, you want predictable rules rather than last-writer-wins corruption. The deeper tradeoffs here, document stores versus relational, optimistic locking, and shared-state ownership, are covered in persistent data and shared state.
4. Real-Time Multiplayer and Lobby State
This is where many teams underestimate the work. Discord scopes an Activity instance to a voice channel and tells you who is connected. You can fetch participants with getInstanceConnectedParticipants() and subscribe to the ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE event to learn when someone joins or leaves. That gives you a roster. It does not give you a game.
Discord provides no real-time state synchronization layer. There is no Discord-hosted authoritative server that resolves who picked up the item first or whose move counts. If your Activity is anything more than turn-by-turn casual, you bring your own real-time backend: a WebSocket server that holds the authoritative game state, accepts player inputs, resolves them, and broadcasts the result to everyone in the instance. Frameworks like Colyseus exist precisely to fill this role, and you can also build it directly on a raw WebSocket server.
Why server-authoritative matters: if the client owns the score, players will edit the score. For anything competitive or with progression on the line, the authoritative state must live on a server the player cannot tamper with. The client sends intents ("I moved here"); the server validates and decides. This is the same principle behind any cheat-resistant multiplayer design.
A practical split for a small multiplayer Activity is two backend roles. A request/response API handles login, profile, leaderboards, and saves. A stateful real-time service holds the per-instance game state over WebSockets. They share the same player identity but have very different scaling and latency profiles. For the architectural patterns behind that split, see multiplayer backend architecture patterns.
5. The Proxy, URL Mappings, and CSP Sandbox
Every network request from your Activity goes through the Discord proxy, and the Content Security Policy limits requests to your own app's proxy. Other Activities cannot read your cookies or make requests on your behalf, which is a sensible isolation guarantee. The practical consequence is that a naive fetch("https://api.example.com/...") from inside the Activity will fail with a CSP error unless that domain has been explicitly allowed.
There are two ways to make external calls work. The first is the URL Mappings section in the Developer Portal, where you map an internal path to an external domain, for example mapping /api to your own backend host. The proxy then routes /.proxy/api/... requests out to your server. The second is the SDK's patchUrlMappings helper, which rewrites the browser's fetch, WebSocket, and XMLHttpRequest globals to match your mappings. Discord's own guidance is to use patchUrlMappings only when necessary, because patching browser globals can have side effects depending on the libraries you pull in.
Networking checklist for an Activity backend:
- Map your backend domain in the Developer Portal URL Mappings before anything else.
- Route your token exchange and API through the mapped path (for example
/.proxy/api/token), not a raw external URL. - WebSocket connections are subject to the same sandbox, so map your real-time host too.
- Test inside the actual Discord client early. The sandbox behaves differently from a plain browser tab.
6. Build vs Buy: Four Honest Options
Once the requirements are clear, the question is how to deliver them. There is no single right answer; it depends on team size, how much of the work is your differentiator, and how fast you need to ship.
Roll your own
A Node or Go service for the OAuth token exchange and API, a database for persistence, and a WebSocket server for real-time state. Maximum control, no per-seat platform fees, and the code matches your game exactly. The cost is that you own auth security, scaling, backups, and uptime. Reasonable when the backend is your differentiator or you already have backend engineers.
Firebase
Firestore for persistence, Cloud Functions for the token exchange, and Realtime Database or a third-party layer for live state. Fast to start and generous at small scale. The friction shows up when you need authoritative server-side game logic, since Firebase is built around client-driven data more than authoritative simulation, and costs can climb with chatty real-time workloads.
PlayFab
A mature LiveOps and player-data platform with leaderboards, inventory, and economy built in. Strong for progression-heavy titles. It is less opinionated about the embedded web context of an Activity and about low-latency authoritative multiplayer, so you typically still pair it with a separate real-time server.
Crux
Crux is a game-backend-as-a-service that bundles identity, persistent player data, and shared state behind one API, with external login providers (including a Discord identity) bridging to a single internal player id. The aim is to collapse the auth bridge, the persistence layer, and shared state into one integration so an Activity team focuses on the game rather than the plumbing. As with PlayFab, evaluate it against your specific real-time latency needs. The honest framing: Crux is one option among these four, not automatically the right one for every Activity.
If you are weighing managed backends generally, our game-backend-as-a-service complete guide lays out the category and what to look for.
Summary: How to Approach an Activity Backend
A Discord Activity is deceptively simple to start and surprisingly demanding once it grows. The SDK gives you identity, presence, and a sandboxed network path. Everything stateful is on you. Get the auth bridge right so a Discord login maps cleanly to your own player. Separate ephemeral session state from durable persistent state and store the durable part under your control. Treat real-time multiplayer as a server-authoritative problem with your own WebSocket backend, because Discord does not sync game state for you. And plan around the proxy and CSP sandbox from day one, since it shapes how every request leaves your app.
Whether you roll your own, lean on Firebase or PlayFab, or use a service like Crux, the four jobs do not change. Pick the approach that matches your team and your latency budget, and validate it inside the real Discord client before you commit.