docs
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help

Build a Discord Activity

A Discord Activity is a hosted web game. Discord runs it in an iframe and connects it to the Discord client through the Embedded App SDK.

Needle output

The discord target creates dist/Discord/. Deploy this folder to an HTTPS web host. Discord does not use a playable-ad ZIP.

1. Create the Discord application

  1. Enable Developer Mode in Discord.
  2. Open the Discord Developer Portal.
  3. Create an application.
  4. Under Installation, enable User Install and Guild Install.
  5. Under OAuth2, add https://127.0.0.1 as a redirect URI for initial testing.
  6. Enable Public Client if the Activity authenticates without a server.
  7. Copy the application ID. This is the public client ID.

Never put a client secret or bot token in the web project. A public client uses PKCE and does not use a client secret. A confidential client exchanges the authorization code on a server.

2. Add the Embedded App SDK

Install the official SDK:

npm install @discord/embedded-app-sdk

Initialize it before you use Discord commands:

import { DiscordSDK } from "@discord/embedded-app-sdk";

const discord = new DiscordSDK("YOUR_APPLICATION_ID");
await discord.ready();

Use the SDK for participant data, channel data, invitations, rich presence, purchases, and instance events. Read the Embedded App SDK reference.

3. Add multiplayer

Discord gives every running Activity an instanceId. Players in the same Activity receive the same value. Use it as the room ID for your game server.

import { DiscordSDK, Events } from "@discord/embedded-app-sdk";

const discord = new DiscordSDK("YOUR_APPLICATION_ID");
const roomId = discord.instanceId;

await discord.ready();
const { participants } = await discord.commands.getInstanceConnectedParticipants();

await discord.subscribe(Events.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE, event => {
    updatePlayerNames(event.participants);
});

instanceId is ready after you create the SDK object. discord.ready() completes the SDK handshake. Authenticate the SDK before you request participant names.

Authenticate without a server

Use a public client with PKCE when the game needs participant names for UI. Keep the token in memory:

function base64Url(bytes: Uint8Array) {
    return btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "");
}

async function authenticatePublicClient(discord: DiscordSDK, clientId: string) {
    const verifier = base64Url(crypto.getRandomValues(new Uint8Array(48)));
    const digest = await crypto.subtle.digest(
        "SHA-256",
        new TextEncoder().encode(verifier),
    );
    const challenge = base64Url(new Uint8Array(digest));

    const { code } = await discord.commands.authorize({
        client_id: clientId,
        response_type: "code",
        prompt: "none",
        scope: ["identify"],
        code_challenge: challenge,
        code_challenge_method: "S256",
    });

    const response = await fetch("https://discord.com/api/oauth2/token", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
            client_id: clientId,
            grant_type: "authorization_code",
            code,
            code_verifier: verifier,
        }),
    });
    if (!response.ok) throw new Error(`Discord OAuth failed (${response.status})`);

    const { access_token } = await response.json();
    await discord.commands.authenticate({ access_token });
}

Call this function after discord.ready() and before getInstanceConnectedParticipants().

Participant data is UI data

Discord states that Activity client data can be changed by the client. Use it for names, avatars, and local presentation. Verify identity on your server before it grants accounts, purchases, saved progress, or other trusted state.

Use a confidential client when your server needs a verified Discord identity. Leave Public Client disabled, exchange the authorization code on the server, and send the access token to the Activity. Read Building Your First Activity for this flow.

After authentication, request and subscribe to the participant list. If Discord returns code 4006, the SDK session is not authenticated or the access token is invalid.

The Embedded App SDK supplies the room ID and participant list. Your game server sends positions, rotations, shots, and game state. Needle networking can send this state.

Connect Needle networking after the SDK is ready. Use the Activity instance ID as part of the room name:

const networkingUrl = `wss://${clientId}.discordsays.com/needle-networking`;

await context.connection.connect(networkingUrl);
context.connection.joinRoom(`my-game:${discord.instanceId}`);

Add these URL mappings in this order:

PrefixTarget
/needle-networkingnetworking-2.needle.tools/socket
/Your game host

Discord sends the first URL to the Needle networking server. The root mapping sends all other requests to the game host.

URL mapping order

Put /needle-networking before /. Discord matches the first suitable prefix. Mapping targets do not include https:// or wss://.

Discord supports WebSockets. It does not support WebRTC or WebTransport. Send only the game state that other players need. Validate player messages before you apply them.

Read Multiplayer Experience and Networking.

4. Add the build target

playableAds: {
    name: "MyGame",
    platforms: [
        { platform: "discord" },
    ],
}

Build the project:

npm run build -- --production

Serve dist/Discord/ from an HTTPS host. Keep all Vite paths relative.

Switch between development and production

Change only the Activity URL setting. Keep the OAuth and networking settings:

Developer Portal settingProxy developmentDirect local developmentProduction
OAuth2 → Public ClientEnabledEnabledEnabled
OAuth2 → Redirect URIhttps://127.0.0.1https://127.0.0.1https://127.0.0.1
Activities → URL Mapping /needle-networkingnetworking-2.needle.tools/socketNot used by the overridenetworking-2.needle.tools/socket
Activities → URL Mapping /HTTPS tunnel hostnameKeep the production hostnameDeployed hostname
Application URL OverrideDisabledhttps://127.0.0.1:5173/Disabled

Mapping targets contain the hostname and path only. Do not add https://, wss://, or index.html. Put /needle-networking before /.

Return to production

Disable Application URL Override and restore the / mapping to the deployed hostname. The OAuth settings do not change.

5. Test through the Discord proxy

Use this workflow to test the SDK, URL mappings, and multiplayer. Discord recommends this workflow because it uses the same proxy as production.

  1. Build the Discord target.

  2. Start an HTTPS tunnel. Discord uses cloudflared in its local development guide:

    cloudflared tunnel --url http://127.0.0.1:5173
  3. Copy the trycloudflare.com host from the terminal.

  4. Serve dist/Discord/ and allow that exact host:

    __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=YOUR_TUNNEL_HOST \
    npx vite preview --host 127.0.0.1 --port 5173 --outDir dist/Discord

    Replace YOUR_TUNNEL_HOST with the hostname only. Vite rejects other host headers.

  5. Open the tunnel URL in a browser. Confirm that it shows the game.

  6. In the Developer Portal, open Activities → URL Mappings.

  7. Add /needle-networking with target networking-2.needle.tools/socket if the game uses Needle networking.

  8. Add / with the tunnel host as its target. Remove https:// from the target.

  9. Open Activities → Settings and enable Activities.

  10. Check that Discord created the default Launch entry point command.

  11. Keep Application URL Override disabled.

Reset the root mapping after the test. A quick-tunnel hostname can be assigned to another user after the tunnel stops.

Development access

An Activity that is not distributed is available only to its owner and developer-team members. Enable Discord Developer Mode to find it in the Developer Activity Shelf.

Discord also supports a direct local URL override. Read Local Development for the HTTPS and platform rules.

6. Use direct local HTTPS

Use a direct URL override to inspect a Vite development build. Desktop Discord requires a trusted HTTPS certificate for this path. Direct traffic does not use Discord URL mappings.

Install vite-plugin-mkcert:

npm install --save-dev --save-exact vite-plugin-mkcert

Enable it only for the Discord development mode:

import mkcert from "vite-plugin-mkcert";

export default defineConfig(({ command, mode }) => ({
    plugins: [
        command === "serve" && mode === "discord" ? mkcert() : null,
        needlePlugins(command, needleConfig, { playableAds }),
    ],
}));

Add a script and start it:

{
    "scripts": {
        "dev:discord": "vite --host 127.0.0.1 --mode discord"
    }
}
npm run dev:discord

The first run creates and installs a local certificate authority. The operating system asks for permission. Open https://127.0.0.1:5173/ in a browser and confirm that the certificate is trusted.

Then open the Activity in Discord and enable Application URL Override. Set it to https://127.0.0.1:5173/. Set the application ID in the build because a local hostname does not contain it.

VITE_DISCORD_CLIENT_ID=YOUR_APPLICATION_ID npm run dev:discord

Do not add Discord launch parameters to the override URL. Discord adds the channel, guild, and Activity instance parameters when it creates the iframe.

Direct networking

Direct local traffic does not use /needle-networking. Connect to the full development WebSocket URL, or use the proxy workflow in the previous section.

If the SDK reports Invalid Origin, disable the override and use the proxy workflow. This confirms the Activity against the same origin and Content Security Policy as production.

7. Test in Discord

  1. Open a test server or direct message.
  2. Open the App Launcher or Activity shelf.
  3. Start the Activity.
  4. Test mouse, touch, resize, and audio.
  5. Join from a second account if the game uses participants or multiplayer.
  6. Check the browser console on desktop.

For multiplayer, start the same Activity instance with two Discord users. Confirm these results:

  • Both clients use the same discord.instanceId.
  • The participant event reports both users.
  • Needle networking reports two users in the room.
  • Each client renders and updates the other player's object.
  • Leaving and joining removes and restores the remote player.

On mobile, enable Developer Mode and open User Settings → Debug Logs. Discord also lets you share application logs from a voice channel.

Read Local Development and Building Your First Activity.

8. Deploy and release

Deploy dist/Discord/ to a stable HTTPS host. To use Needle Cloud, follow Deploy from the CLI and select dist/Discord/ as the deployment directory.

In Activities → URL Mappings, map / to the deployed hostname. Do not include https:// and do not target index.html. Keep Application URL Override disabled.

Test this configuration through the Developer Activity Shelf. For public access, complete Discord's discovery and distribution setup.

Ask an agent to test

Copy this prompt and provide a Discord test application:

Test my Discord Activity from dist/Discord using the Discord test application I provide.

1. Build dist/Discord and run the platform validator.
2. Start vite preview and cloudflared with the commands in this guide.
3. Set the root URL mapping to the tunnel host. Keep the networking mapping before it.
4. Connect to Discord Desktop through its browser debugging port and open the Activity from the Developer Activity Shelf.
5. Inspect the Activity iframe, not only the main Discord window. Confirm that the canvas renders non-black pixels and changes after input.
6. Confirm this SDK order: ready, authorize, token exchange, authenticate, participant request.
7. Confirm that the participant list contains the signed-in user's display name and that no request is blocked by Content Security Policy.
8. Start a second client in the same Activity instance. Confirm two room users, two participant names, and two rendered player objects.
9. Test input, resize, audio unlock, reconnect, and leave cleanup.
10. Restore the production root mapping after the test.

Report the Discord client version, Activity application ID, mapping targets, build commit, rendered-frame evidence, SDK event order, multiplayer result, and exact errors. Restore the original URL mapping when the test is complete.

Network requests

Discord routes Activity traffic through its proxy. Add one URL mapping for each external service. Call the mapped path from the game. For example, map /api to api.example.com, then call /api from the Activity.

Put specific paths before the root / mapping. This lets /needle-networking reach the WebSocket server while / reaches the game host.

Read Discord URL Mapping.

External packages

Check packages for hard-coded external URLs. Discord blocks an unmapped request with blocked:csp. Keep files in the Discord build when the package can load them locally. Add a mapping only when the service must stay online.

Related pages

  • Build Playable Ads and Hosted Games
  • Discord Activities overview
  • How Activities Work
  • Production readiness
  • Discovery and distribution
  • Needle Cloud
Suggest changes
Last Updated: 8/31/26, 1:41 PM

Extras

Needle AI Ask Needle AI
Copy Markdown

Navigation

  • Getting Started
  • Tutorials
  • How-To Guides
  • Explanation
  • Reference
  • Help

Extras

Needle AI Ask Needle AI
Copy Markdown