Wiring with Discord

The integration box on The World: Wiring, opened up for Discord: who talks to whom when the bar's people come from a Discord server. One process serves one Discord server (COMMUNITY_ID); each room is a page at /<roomid>, and a room may have a channel of its own as its window.

runs on your server    Discord-owned    plain arrows: a person's own client
your server · bar.example.com Discord Your browser The World web client canvas room, chat box, look picker Anyone's Discord app desktop or phone, no web client chats in the channel as usual and sees the pinned picture of the room OpenRouter the model behind Harry (Gemini Flash Lite) outbound HTTPS from harryd only Apache TLS (Let's Encrypt) · reverse proxy for / and /ws palacebot (one Node process, :3311) HTTP · OAuth2 · sessions login callback, /api/me, serves the client Snapshot renderer PNG of the room every 20 s when changed; Harry's eyes too Discord bot (discord.js) mirrors room ↔ channel, webhook queue, sleepers WebSocket room server people, balloons, scenes, paint, widgets, games SQLite users (+ prefs), sessions, settings (rooms, spots, top tens), agent log OAuth2 discord.com/oauth2/authorize REST API oauth2/token · users/@me guilds/…/members channels/…/messages (edit) webhooks/… (execute) application commands Gateway events pushed to the bot #palace (one channel per server) · regular chat from anyone · webhook posts (if mirroring is on) · pinned snapshot + "Enter" link · /palace peek|who|say, /harry (ephemeral) bot ignores its own webhook posts HTTPS page · /api/me wss /ws moves · talk · scene proxies both to :3311 "Log in with Discord": browser is redirected to OAuth2 (scope: identify), then back to /auth/callback with a code code → token → who is this, member of the guild? edit the pinned snapshot message optional: post room talk to the channel (webhook; OFF by default, per room) Gateway wss bot dials out, so no inbound port on your server messages, typing, member updates ordinary Discord chat; posts here become eyes-closed sleepers in the room sees the pinned picture (with balloons, boards, games) and the "Enter " button new messages flow up to the bot harryd (second Node process) Harry: prompt, tool loop, cost meter, budgets Harry's OpenRouter key, from the Keys tab or its .env; never the Discord token joins the room like a browser, with an agent token ws /ws · agent token events in, checked calls out HTTPS chat + tools
Every link, with its protocol. The browser never talks to Discord directly except for the one-time login redirect; everything else goes through the palacebot process on your server. Discord never connects inbound to your server: the bot opens the Gateway websocket outward, so only Apache's 443 is exposed. Room talk stays in the room by default (dashed arrow): the channel sees the bar through the pinned picture, and people in the channel appear in the room as sleepers.

The links, one per row

From → ToProtocolCarries
Browser → Apache → palacebotHTTPS :443 → :3311The page and assets, /api/me (am I logged in, and what token do I use for the socket), the OAuth callback.
Browser ↔ Apache ↔ palacebotwss /wsEverything live: hello, moves, talk, whispers, face/color/prop changes, paint strokes, votes, piece drags, lists (the user list and the room list, asked for while the 👥/🚪 drawer is open, every ten seconds; a person's id is sent only to people in the same room); back: welcome, joins, sleepers, balloons, captions, scene and widget state, and retract when the house takes a line back. The socket reconnects by itself with a backoff, so a dropped connection or a server restart heals without anyone touching the page. A restarting message goes out first when the server is stopped on purpose: the page then says the bar will be right back and retries about once a second instead of backing off, and when it gets back in it reloads itself only if the server is now serving a different client bundle than the page is running.
harryd ↔ palacebotws /ws (same host)Harry's seat, one connection per room (harryd checks /api/config every minute for new rooms, and /api/agent/config, with its agent token, for his OpenRouter key and running config from the settings screen, applied live): an agent token instead of a login. In: talk, whispers to him, joins and leaves (with where from), widget and game events, and a handoff note from himself in the room that sent people over. Out: an enumerated set of calls the server checks (say, whisper, narrate, scene, paint, widgets, games), most of them only while a host has opened the floor; room.transport, loose.clear and the puppet.* calls (add, remove, move, chat: the test puppets, persons with a bot flag that the room treats like anyone) need only a host's recent word. A line or whisper to a puppet Harry chats as reaches him marked to, and he answers with say/whisper as.
harryd → OpenRouterHTTPSOne chat completion per turn with tools, sometimes with a PNG of the room attached, or a picture from the wall when someone asks about it. Cost comes back per call and is metered against the room's daily budget. The key is Harry's own (the Keys tab, else harryd's .env), not automoderation's.
palacebot → OpenRouterHTTPS (outbound)Automoderation, only when the Moderation tab says AI and a key is on the Keys tab (its own key, not harryd's): lines said out loud in the bar, when OpenRouter is the chat judge, in bunches of up to ten, with a fixed prompt that answers PG, R or MA for each, after the lines have been shown; and each new picture (pasted, by address, or found on the web) as a data URL before it's stored, for one word back. No tools, temperature 0, twenty-second timeout. Cost comes back per call and is counted server-wide against the day's budget; past it the calls stop until tomorrow. Whispers, Harry's lines and Discord's never go.
palacebot → TypeSafeHTTPS (outbound)Automoderation when Jev is the chat judge and a TypeSafe key is on the Keys tab: each line said out loud, alone and at once after it has been shown, as the state of one Score question over the five-level scale; back come the probabilities, their average and a confidence, nothing generated. Ten-second timeout. Input tokens are counted at the price in .env into the same daily budget. The same lines that never go to OpenRouter never go here. With the 8-ball in (Harry, 8ball on), a line said to Harry goes here instead of to harryd: first as five yes/no questions about what kind of question it is (yes/no, this-or-that, a degree, a number, and is the answer yes), then, for this-or-that and degree, a second request with a typed question built in code (a Choice over the words the question itself offered, or a Score over a five-level scale around the question's own adjective, ten levels for "out of ten"); the room server speaks a stock line for the answer. Nothing is generated; harryd hears nothing until the ball is out.
palacebot → Wikimedia / OpenverseHTTPS (outbound)"Find a picture of X": a search query to the provider's API, then one fetch of the chosen image into data/pictures/, re-encoded. The server fetches only URLs a provider returned, https, public addresses, images under 8 MB. Browsers get the copy from palacebot at /pictures/, never from the source site.
Browser → palacebotHTTPS /api/images, /api/propsA pasted picture, an https address, or an emoji becomes a stored PNG (normalized, hashed, deduplicated) and a prop (image + offset + hides-face); served back same-origin at /avatars/. Emoji come from Twemoji's CDN, addresses through the fetch guard.
Browser → palacebotHTTPS /api/settings, /api/me/prefsThe settings screen: a JSON view of the room's settings and a merge-patch back, each part gated by its flag (room_authoring for the room and for POST /api/settings/rooms, which writes rooms/<id>.json, asks Discord for a channel only when one is wanted (most rooms have none: the guild is the door, and there's no window), and starts the room; manage_moderator_settings for the People tab, and manage_admin_settings for the house rules, each key including the one before). Harry's outfit goes the same way from his menu in the bar. /api/me/prefs keeps a person's own small preferences (what they've worn lately, their emoji) on the server so they follow them between devices; one's rating and word list go over the socket (meSettings) and come back in the welcome.
Browser → Discord OAuth2 → BrowserHTTPS redirectsOne-time login. Discord sends the browser back with a short-lived code.
palacebot → Discord RESTHTTPSCode → access token → user id, then "is this person in the guild and can they see the channel?". The access token is thrown away after that.
palacebot ↔ Discord Gatewaywss (outbound)The bot's live feed: messageCreate, typingStart, guildMemberUpdate. This is how sleepers appear.
palacebot → Discord REST (webhook)HTTPSOnly if a room sets mirrorToDiscord: public room talk posted under the speaker's name. Off by default, since half-conversations were noise for the channel. Whispers, moves and looks never travel here regardless.
palacebot → Discord REST (snapshot)HTTPSEdits the one pinned message with a fresh PNG of the room, balloons, boards and games included, at most every 20 s and only when something changed. This is the channel's window into the bar.
Discord app → palacebot (slash)Interactions/palace peek|who|say for anyone; /harry status|on|off|log|reset|play for barkeeps and /harry mute|ban|pardon|bless|floor|punished for barowners (the room checks the tier itself). Answers are ephemeral. say carries a line into the room as a caption under the sender's name and, when it addresses Harry, waits up to fifteen seconds for his reply and returns it to the sender alone.
Discord app ↔ #harrys-barDiscord's ownOrdinary chat. Nothing to install. Lurkers see the pinned picture and the webhook posts.

Why there is no echo

Room talk goes out through a webhook, and the bot drops every incoming message that has a webhook id or a bot author. So the bot's own posts come back through the Gateway and are ignored.

One head per person

A Discord user id is the key everywhere. Connected on the web: open eyes. Only chatting from Discord: a sleeper with eyes closed at a saved seat. Connecting converts the sleeper in place.

Harry, the agent process

A second process, harryd, connects to the room server over the same WebSocket as a browser does, with an agent token instead of a login. It receives room events and issues only calls from an enumerated list the server checks; changing the room needs a host to have opened the floor within the last ninety seconds. It alone holds the OpenRouter key; the room server alone holds the Discord token. Whispers between people never reach it, and nothing Harry says goes to Discord except through the pinned picture.

Games run on the server

Wheel of Cheese, hangman, the boards, timers, dice and polls are server code with their state in the scene; Harry can start them and hears about them but never decides an outcome. Twenty questions is the one game that lives in harryd: a tally of his own questions, kept beside the transcript rather than in it, so it survives the transcript's trimming. A guessing game runs faster than his say limit (six lines a minute), so one reply per line said to the room is free of that limit, and a line the bar does refuse is sent again after ten seconds rather than lost. The game show has its own page: how Wheel of Cheese is wired.