The World: Wiring
Who talks to whom to make The World work: the browser, the room server, and the integration, the service its people come from (Discord), or none. One process serves one community; each room is a page at /<roomid>. Discord's own links, one by one, are on Wiring with Discord.
INTEGRATION=local people come in by invitation with a name and a password, their tiers are blessings, and there is no window. Invitations work beside Discord too. Everything past the four points — the room, Harry, games, moderation, the database — neither knows nor cares which integration is behind them.The links, one per row
| From → To | Protocol | Carries |
|---|---|---|
| Browser → Apache → palacebot | HTTPS :443 → :3311 | The page and assets, /api/me (am I logged in, and what token do I use for the socket), the OAuth callback. |
| Browser ↔ Apache ↔ palacebot | wss /ws | Everything live: hello (with the browser's public keys, when it can seal), moves, talk, whispers (sealed between people: enc, bytes the server passes on and can't open), a bolted room's sealed talk and its key going round (roomKey, wrapped to each person; keyAsk from someone who hasn't got it), 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 ↔ palacebot | ws /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 → OpenRouter | HTTPS | One 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 → OpenRouter | HTTPS (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 → TypeSafe | HTTPS (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 / Openverse | HTTPS (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 → palacebot | HTTPS /api/images, /api/props | A 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 → palacebot | HTTPS /api/settings, /api/me/prefs | The 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 → the integration → palacebot | HTTPS redirects | Login. The browser goes to the integration's login and comes back to /auth/callback; the integration's part ends at a user id in its own namespace (discord:…), and the session and cookie are the bar's. Discord: OAuth2, scope identify. |
| Browser → palacebot | HTTPS /invite, /auth/local, /reset | Invitations, on any install: a barowner's one-use link makes a local:… account with a chosen name and a password (scrypt; the link kept only as a hash), and /auth/local signs it back in. A password link from the People tab sets a new password. With no integration, this is the only way in. |
| palacebot → the integration | the service's API | The door and standing, asked together when someone connects and cached five minutes: is this id one of the community's, may they see this room's window, and what tier do their roles give them. A blessing raises the tier either way. Invited people skip the integration's door: the invitation was theirs. Discord: guilds/…/members, and member updates live over the Gateway. |
| palacebot ↔ the integration | the service's API | The window, optional and graded: a picture of the room kept fresh, talk posted out under each speaker's name (per room, off by default), chat from over there arriving as eyes-closed sleepers, and commands from there. Discord has all four; see Wiring with Discord for its seven links. Without a window the room is seen only in the browser. |
The integration is a seam, not a layer
Its code lives in packages/server/src/integrations/<name> and is chosen by INTEGRATION. The room server calls one resolver and gets a name and a tier back; it never learns which service answered. A room file says which community it belongs to as access: { provider, community, window }.
One head per person
A user id, namespaced by where it came from (discord:, local:), is the key everywhere. Connected on the web: open eyes. Only chatting from the window: 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.