PalaceBot Wiring

Who talks to whom to make Harry's Bar work: the browser, the PalaceBot server, and Discord. One process serves several Discord servers; each gets its own room at /<roomid>.

runs on your server    Discord-owned    plain arrows: a person's own client
your server · bar.example.com Discord Your browser Harry's Bar 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, ghosts WebSocket room server people, balloons, scenes, paint, widgets, games SQLite users, sessions, settings (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, /palace-who, /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 ghosts in the room sees the pinned picture (with balloons, boards, games) and the "Enter Harry's Bar" link new messages flow up to the bot harryd (second Node process) Harry: prompt, tool loop, cost meter, budgets holds the OpenRouter key; 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 ghosts.

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; back: welcome, joins, ghosts, balloons, captions, scene and widget state.
harryd ↔ palacebotws /ws (same host)Harry's seat: an agent token instead of a login. In: talk, whispers to him, joins and leaves, widget and game events. 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.
harryd → OpenRouterHTTPSOne chat completion per turn with tools, sometimes with a PNG of the room attached. Cost comes back per call and is metered against the room's daily budget. Only harryd has this key.
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 ghosts 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 and /palace-who for anyone; /harry status|on|off|log|reset|play for hosts. Answers are ephemeral.
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 ghost with eyes closed at a saved seat. Connecting converts the ghost 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. The game show has its own page: how Wheel of Cheese is wired.