> For the complete documentation index, see [llms.txt](https://docs.kouch.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kouch.dev/docs/discord_plan.md).

# Discord in Kouch: friends, invites, account sync, profile picture and banner (2026-09-28)

An addendum; it doesn't replace any plan. The owner (2026-09-28): *"we are also using discord for invites, friends and maybe also chat, or probably not, user syncing would be best, and have the app choose between profiles, if use the discord pfp and banner (if possible) or steams"*. They supplied the Discord Social SDK 1.10.19337 (`DiscordSocialSdk-1.10.19337.zip`: C API `cdiscord.h`, `discord_partner_sdk.dll`, `libdiscord_partner_sdk.so`). Decision record: ADR 0022.

## What the SDK gives (checked against `cdiscord.h` and Discord's Social SDK guides, 2026-09-28)

* **Platforms:**
  * Windows x64 is supported.
  * Linux x64 is "experimental", with glibc 2.31 or newer. SteamOS qualifies but isn't named, so the Deck must be tested.
* **Threading:** callbacks come through `Discord_RunCallbacks()` pumped from one thread (or `Discord_SetFreeThreaded()`), so one Discord thread owns the client, as the input thread owns Steam Input.
* **Sign-in:**
  * The public client uses PKCE (`CreateAuthorizationCodeVerifier` + `Authorize` + `GetToken`). It needs the Application ID only, **never a client secret in the app**.
  * The device flow (`OpenAuthorizeDeviceScreen` / `GetTokenFromDevice`: a QR code and a short code; approve on a phone at discord.com/activate) is in the desktop headers. Discord documents it for consoles, so whether it's allowed for a desktop/Deck app must be confirmed with Discord.
  * Tokens last 7 days, with `RefreshToken` to renew them.
* **Friends:**
  * Relationships come with Discord and in-game types, capped at 1,000 friends.
  * Presence is `Status()`, and the game activity is only this app's.
  * Avatars come from `UserHandle_AvatarUrl` (png/webp/gif).
  * **Banners aren't in the SDK.** They come from REST `GET /users/@me` (`identify` scope, the user's own token) and Discord's CDN.
* **Invites:** rich presence with a party and a join secret, `SendActivityInvite`, the ActivityJoin/InviteCreated callbacks, and `RegisterLaunchCommand(app_id, "kouch://…")` or `RegisterLaunchSteamApplication` so "Join" starts Kouch.
* **DMs:** `SendUserMessage`, sent only on a user action. There is a dev-tier limit of 100 messages per 2 h per app until the "Comms Access" review passes.
* **Policy:**
  * Discord's friend data is used in-session only and never persisted; unlinking removes it at once.
  * Discord friends are shown alongside the in-game (Steam) friends, which Discord expects.
  * The Discord button follows the official branding (unmodified assets, one of the four button styles).
  * Two policy pages (Developer Terms, Developer Policy) could not be fetched automatically. The owner reads them before release, including the terms for redistributing the DLL/.so.

## Both networks, fully (owner 2026-09-28)

"we need proper steam chat, invites, friends, and also for discord, and show an indicator to differenciate each".

* **Steam:** friends, presence, invites and in-app chat already exist (C's social work). The whole path must be proven with a second Steam account: send and receive a chat, send an invite, accept it and land in the lobby. Invites and rich presence need Kouch's own app id; the rest works under 480 among testers.
* **Discord:** the same set: friends, presence, invites, rich presence, and chat through `SendUserMessage`. Chat is back in scope, so DC6 is no longer optional.
* **Which network a friend is on:** a small mark, not a colour dot (presence already uses colour). The two brands' rules (checked 2026-09-28) decide its form:
  * **Discord:** the official Discord symbol, unmodified, never recoloured. That means the official White variant on Black and the Black variant on White, from the official symbol kits on discord.com/branding, stored in the app. It sits in the avatar's lower-left corner on a `--k-surface` disc (14 px at 1280×800), with `aria-label` "on Discord". The presence dot keeps the lower-right corner. Discord allows the symbol where its brand is established elsewhere, which Settings › Accounts › "Link Discord" (official button) does.
  * **Steam:** no logo on avatars. Valve's brand guidelines say the Steam logo "must stand alone and may not be combined with any object" and reserve approval of any use, so a logo on an avatar's corner isn't allowed. Steam is Kouch's home network, so a friend without a mark is a Steam friend. Where both networks show together (the friend sheet, the chat header), the network is written out as text ("on Steam", "on Discord").
  * A person linked on both networks shows once, with the Discord mark, and the friend sheet lists both.

## One person, one conversation (owner 2026-09-29)

The friends and chat views merge the two networks by person, not by account:

* **Linking two accounts as one person** is the user's choice, made once on the friend sheet ("Same person on Discord…"). Kouch never guesses by name or picture. The link lives in local settings only (a Steam id ↔ Discord id pair; nothing about the friend besides the two ids). Unlinking Discord removes every pair.
* **One row per person.** A linked person shows once. The Discord mark is the only network sign (no Steam logo; see above), and the friend sheet names both networks as text. The groups are **Messages** (unread first), **In Kouch**, **Online elsewhere** and **Offline**.
* **One conversation per person.** Both networks' messages go into one thread, ordered by time, each with a quiet "on Steam" / "on Discord" note where the network changes.
* **Where a message goes** (the composer shows the choice as a small Steam | Discord switch, always visible when both exist):
  1. Discord not linked: Steam.
  2. The friend is on one network only: that network.
  3. Both: Steam when the friend has Kouch open (their rich presence says so), else Discord. When neither says, it's the network they last wrote on.
  4. The switch overrides it for this conversation until the sheet closes.
  5. A message is sent on one network only, never both, and only when the user presses Send (Discord's rule).
* **Invites follow the same rule.** The invite card (`components/social/InviteCard.svelte`) draws a Steam lobby invite and a Discord activity invite alike; Discord's Join goes through the launch command each install registers, then the usual join checks. A toast carries "Hold to join" with the invite's countdown.
* Built and checked against `?mock=discord` (a linked pair, both networks' messages in one thread) before the owner's sign-in.

## My profile from Steam or Discord (owner 2026-09-29)

* **One switch:** Settings › Accounts › "Show my profile from: Steam | Discord", default Steam. Discord is disabled, with a "Link Discord" action beside it, until linked. It replaces the separate picture and banner choice (DC5).
* **What Discord gives**, read-only from `GET /users/@me` (`identify` scope, the user's own token, only their own profile):
  * `username` and `global_name`;
  * `avatar` (an `a_` hash is animated);
  * `banner` and `accent_color`;
  * `avatar_decoration_data`, drawn like a Steam avatar frame (its ratio measured, not assumed);
  * `collectibles.nameplate` and `primary_guild`.
* **Cache:** the same disk cache as Steam's profile items (`<data>/cache/social/`), which takes files only from `cdn.discordapp.com` and `media.discordapp.net`. It refreshes on sign-in, at start and every few hours, and unlinking clears it. Friends' Discord data is still never written to disk.
* The animated-avatar setting (Settings › Display) holds a Discord animated avatar and decoration still, exactly like Steam's.

## Parts

| #   | Part                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Owner                                         |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| DC0 | ADR 0022, this plan; the owner creates the Discord app (below)                                                                                                                                                                                                                                                                                                                                                                                                                                                           | central, owner                                |
| DC1 | `crates/kouch-discord`: the SDK loaded at run time (`libloading`; no link-time dependency, so Kouch runs without the SDK and Discord features are simply off), a safe wrapper over the used C calls on one "kouch-discord" thread that pumps `Discord_RunCallbacks`, channels like `InputCmd`. The unsafe island is this crate only                                                                                                                                                                                      | C                                             |
| DC2 | Sign-in / account link: Settings › Accounts › "Link Discord"; on the desktop the PKCE flow in the browser; on the Deck/Game Mode the device flow (QR + code), if Discord confirms it for us, else the PKCE flow in Steam's browser overlay or a phone link. Tokens in the OS keyring (the N2 secrets store, per config dir); unlink wipes token and every Discord datum                                                                                                                                                  | C                                             |
| DC3 | Friends: Discord friends merged into the Friends widget/feed/sheets beside Steam's, one person shown once when both are linked (by the user's choice, not guessed), a small source badge; presence and "in Kouch playing X" from rich presence; nothing stored to disk                                                                                                                                                                                                                                                   | C                                             |
| DC4 | Rich presence on **both Discord and Steam** (owner 2026-09-28) + invites: Kouch sets its activity (game, system, party size) on Discord and Steam's rich presence (`SetRichPresence`, `steam_display` tokens; Steam's needs Kouch's own app id, so it's guarded by `has_own_app_id()` until then) with a join secret that maps to the existing Steam lobby or a Discord lobby; "Invite via Discord" in the game menu and friend sheet; Join lands in `kouch://` and runs the existing join checks (same game hash, mods) | C (B for the join plumbing)                   |
| DC5 | Profile chooser: Settings › Profile: picture and banner from Steam or Discord (banner via REST with the user's token), shown in the top bar, profile card, Home and the friends' view of you where possible; the choice syncs with the other settings                                                                                                                                                                                                                                                                    | A (UI), C (data)                              |
| DC6 | DMs: the existing chat sheet grows a Discord source beside Steam's, with the network mark in its header; sent only on a user action (Discord's rule)                                                                                                                                                                                                                                                                                                                                                                     | C                                             |
| DC7 | Parity + checks: Windows and the Deck (Desktop and Game Mode), no Discord client running, Discord client running, SDK missing (features off, no error), unlink wipes everything; fit/align/Deck checks on every new sheet                                                                                                                                                                                                                                                                                                | central                                       |
| DC8 | One person, one conversation: user-made Steam ↔ Discord pairs, merged rows and groups, one thread per person, the routing rule and the composer's Steam \| Discord switch, invites by the same rule (above)                                                                                                                                                                                                                                                                                                              | C                                             |
| DC9 | My profile from Steam or Discord: the one switch, the `/users/@me` fields, the disk cache, the decoration as a frame (above)                                                                                                                                                                                                                                                                                                                                                                                             | C (data, Accounts), A (top bar, profile card) |

## The owner needs to do

1. **Create the app** in the Discord Developer Portal. Keep its **Application ID** (not a secret) and send only that.
2. **OAuth2:** turn on "Public Client" and add the redirect `http://127.0.0.1/callback`.
3. **Keep the client secret to yourself.** Kouch never ships it.
4. **Later:**
   * Read the Developer Terms and Developer Policy (the redistribution terms for the DLL/.so).
   * Apply for Comms Access once linking, presence, invites and friends work: it needs a 1–5 min video, and it lifts the dev-tier rate limits.
   * Ask Discord whether the device sign-in flow is allowed for a desktop/Deck app.

## Rules carried over

* **The SDK stays out of git**, like the Steamworks SDK: the zip and its tree live under `vendor/discord_social_sdk*` (git-ignored). A release copies the DLL/.so beside the binary, once redistribution is confirmed.
* **No Discord brand names in Kouch's own identifiers beyond what integration needs.** The Discord logo appears only through Discord's official button assets. (Discord isn't a console brand, so the brand lint doesn't apply; its branding rules do.)
* **Nothing about a friend is persisted.** A linked account's token lives in the OS keyring only.

## Status

| Part | Status                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DC0  | done: plan, ADR 0022; Application ID `1554108174108196884` (public; goes in `kouch-discord` as a constant, overridable by `KOUCH_DISCORD_APP_ID` for tests). The portal's public key is for interaction webhooks and isn't used                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| DC1  | built: `crates/kouch-discord` loads the SDK at run time (libloading 0.7, already in the tree), one "kouch-discord" thread owns the client and pumps `Discord_RunCallbacks`, commands/events over channels; the real SDK loads and makes a client on Windows and in the Linux container; without it `Unavailable` and nothing else                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| DC2  | built: the SDK side (`DiscordCmd::Link` PKCE → Authorize → GetToken → UpdateToken + Connect, `Restore` refreshing within a day, `Unlink` revoke + disconnect, redacted `Secret` tokens, `kouch-lab discord-link`), and the app side: `discord.rs` starts the thread, keeps the pair in B's local-only record (`secrets::local_record("discord")`, keyring or a machine-sealed file, never Steam Cloud), restores it at start; Settings › Accounts links/unlinks (`discord_status`/`discord_link`/`discord_unlink`, `discord:status`). Live sign-in waits for the owner                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| DC3  | built: the Discord thread lists friends (Discord or in-game friends; not blocked or pending) on Ready and on every relationship/user change (at most every 500 ms); the shell wires them in the UI's friend shape (`network: "discord"`, id `discord:<id>`, presence onto Steam's words), fetches their pictures from the CDN into memory only (https, ≤ 512 kB, online friends), `discord:friends` + `discord_friends`; the social store keeps them beside Steam's and drops them with their pictures on unlink; `FriendAvatar` marks them with Discord's unmodified symbol lower-left (14 px at 1280 × 800); `?mock=discord`. Live with a real account after the owner's sign-in                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| DC4  | built: Steam's rich presence already existed (guarded by `has_own_app_id()`); Discord now shows the game while one runs and an online session as a private party (size from the lobby, max from the profile) whose join secret names the Steam lobby (`kouch-lobby:<id>`); the activity is re-applied on every connect and sent only when it changes; Discord's join lands as the usual `netplay:join_request` (same checks); the friend sheet invites a Discord friend on Discord (`discord_invite`, toast with the outcome); the launch command registers Kouch's executable. Live with two accounts after the owner's sign-in                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| DC5  | data built (C): after sign-in the shell reads the account's picture (the SDK's avatar URL) and its banner and accent colour (REST `GET /users/@me` with the user's own token; the banner from Discord's CDN), keeps them in memory only as data URLs, and puts them on `discord:status` (`avatar`, `banner`, `accent_color`); Settings › Accounts shows the picture. The chooser itself (Steam or Discord picture/banner) is A's                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| DC6  | built: `DiscordCmd::SendMessage` → `Discord_Client_SendUserMessage`, only from the chat sheet's Send (`discord_chat_send`, `discord:` ids only, linked only); sent and received direct messages come back through the SDK's message-created callback as `discord:chat` in the chat sheet's shape (conversation `friend:discord:<id>`, id `discord-<message id>`, one copy each), in memory only; a failed send is a toast. With Discord linked, the chat header and the friend sheet say "on Discord" / "on Steam" as text (no Steam logo). `?mock=discord` answers after 0.8 s. Live with two accounts after the owner's sign-in                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| DC8  | built (C, `a172828`, `586bd7e`, and incoming invites): pairs in Settings through the friend sheet (`settings.social.people`, cleared on unlink), one row and one thread per person (`lib/people.ts`), `routeFor` for messages and invites, the chat's "Send on" switch; a Discord friend's invite (the SDK's invite callbacks, Kouch's own join invites only, once per message) joins Steam's invite queue from `discord:<id>`, so it's checked as a Kouch lobby and shown like a Steam one (toast, Friends widget, the chat's card on Kouch's invite words). Live with the owner's sign-in and a second Discord account                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| DC9  | built: data + switch (C, `8e933f9`): `discord_me.rs` reads `/users/@me` (names, picture + animated, banner + animated, accent, decoration + still, nameplate, server tag; expired items dropped, only Discord's asset paths become URLs), keeps it in `<data>/cache/social/discord-me.json` with its files from Discord's CDN only (served through `kmedia`), shows it at start while a link is kept, refreshes at each sign-in and every 4 h, and unlinking deletes it; `DiscordStatus.profile`; `settings.profile.source` (unset = DC5's `picture`); `lib/me.ts` `resolveMe` gives every field with per-field fallback and stills when pictures may not move; Settings › Accounts has the one switch with Link Discord beside it (DC5's two rows are gone); `?mock=discord-me`. Top bar and profile card (A, `bea358a`): your name and picture with its moving layer and the decoration as its frame (1.2, round), the animated banner, the nameplate behind the name, the server tag; sweep cells `steam-profile-discord`, `home-discord-me`. Live after the owner's sign-in, where the decoration's 1.2 ratio gets checked against a real one |
| DC7  | waiting for the owner: the portal settings (Public Client, the `http://127.0.0.1/callback` redirect), one sign-in (`cargo run -p kouch-lab -- discord-link`), then a second Discord account for invites and DMs; the Deck run follows (Linux is "experimental" in Discord's matrix)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kouch.dev/docs/discord_plan.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
