Running Your Own Bar
PalaceBot is one Node process: a room server, a small web app, and a Discord bot. This walks through a fresh install, from the Discord Developer Portal to a public HTTPS address. Budget an hour.
- What you need
- 1. Discord application
- 2. Invite the bot
- 3. Run it locally
- 4. Configure a room
- 5. Put it on a server
- 6. First boot checklist
- Beyond one room
- Enabling Harry
What you need
- A Discord server you administer.
- Node 22 or newer, and git.
- For the public version: a small Linux host you can SSH into, a domain name pointing at it, and a reverse proxy that speaks WebSockets. The deploy files assume Apache with certbot on Ubuntu, but anything that proxies HTTP and
/wsto a local port works.
1. Create the Discord application
- Go to discord.com/developers/applications and click New Application. Name it whatever your bot should be called.
- General Information: copy the Application ID. That is your
DISCORD_CLIENT_ID. - OAuth2: copy (or reset) the Client Secret into
DISCORD_CLIENT_SECRET. Under Redirects addhttps://bar.example.com/auth/callbackfor production andhttp://localhost:5173/auth/callbackfor local development. Discord only accepts callbacks that match this list exactly. - Bot: click Reset Token and copy the token into
DISCORD_TOKEN. It is shown once. Then turn on both Server Members Intent and Message Content Intent under Privileged Gateway Intents; the bot needs them to see who is in the channel and what they say. Consider unticking Public Bot so only you can invite it.
2. Invite the bot to your server
Open this URL with your own Application ID in place of CLIENT_ID, pick your server, and approve:
https://discord.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot%20applications.commands&permissions=537259008
The permissions number grants View Channels, Send Messages, Manage Webhooks, Manage Messages, Read Message History, Attach Files, Embed Links, and Use External Emojis. Manage Webhooks is how room talk gets posted under people's own names; Manage Messages is only used to pin the room snapshot.
If the bar's channel is private, add the bot's role to that channel explicitly. Channel overrides can hide a channel even from a role with server-wide View Channels.
3. Run it locally
git clone <this repo> palacebot && cd palacebot
npm install
cp .env.example .env # fill in the three Discord values from step 1
cp rooms/example.json.sample rooms/myroom.json
npm run dev
The server listens on port 3000 and the Vite dev server on 5173. Open http://localhost:5173/myroom. With Discord credentials in .env you can log in for real; without them the server runs in a Discord-less mode and ?dev=Name on the URL lets you in as a test user. npm run fake-users -- 8 fills the room with wandering bots for layout testing.
4. Configure a room
Each file in rooms/ is one room, tied to one channel on one Discord server. Edit your copy of the sample:
id: the room's URL slug, e.g.myroommakes/myroom. Must match the file name.guildIdandchannelId: in Discord, turn on Developer Mode (Settings → Advanced), then right-click the server and the text channel and choose Copy ID. Together they are the room's door: only members of that server who can see that channel get in. The same can be written as anaccesspolicy block; that form exists so a room could later use a different door.name: what the room is called in the page title, the pinned message, and the slash commands.painting: who may paint on the room outside a game:"off","hosts"(default), or"anyone". Harry can widen it during a session.mirrorToDiscord:trueto post room talk into the channel under each speaker's name. Defaultfalse; the pinned picture shows the balloons instead, and the channel stays quiet.background: a 512×384 image underassets/. The bundled one is Harry's Bar from the 1995 Palace.capacity: how many people can be in the room at once. Twenty is already generous; the original defaulted to sixteen.spawnSpots,ghostSpots,floor: where newcomers appear, where people chatting from Discord are seated, and the clickable rect. All in room pixels.
5. Put it on a server
The deploy/ folder has a systemd unit, two Apache virtual hosts, and a sync script. On the host, one time:
# Node 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs build-essential
# Apache modules and the HTTP vhost (edit the ServerName first)
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
sudo mkdir -p /var/log/apache2/palacebot
sudo cp deploy/apache-palacebot.conf /etc/apache2/sites-available/023-palacebot.conf
sudo a2ensite 023-palacebot && sudo systemctl reload apache2
sudo certbot --apache -d bar.example.com
# replace the generated SSL vhost with deploy/apache-palacebot-le-ssl.conf (proxy + WebSocket lines)
sudo cp deploy/palacebot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable palacebot
On your own machine, copy deploy/local.env.example to deploy/local.env with your SSH host and target directory, then run deploy/deploy.sh. It rsyncs the repo (without .env, data, or build output), installs, builds, and restarts the service. Put the production .env on the host by hand with PUBLIC_URL=https://bar.example.com, a long random SESSION_SECRET, PORT=3311, and NODE_ENV=production. Your rooms/*.json files are synced along with everything else.
6. First boot checklist
Watch sudo journalctl -u palacebot -f on the first start. In order you should see the bot log in, then per room: ghosts seeded from recent history, "mirroring #channel in Server", and finally "ready". On first contact with a channel the bot creates a webhook named after itself, posts the room snapshot, pins it, and registers /palace and /palace-who.
- Missing Permissions on webhooks: the bot's role lacks Manage Webhooks on that channel. Fix the role or re-run the invite URL.
- could not pin snapshot message: harmless; the snapshot exists but isn't pinned. Grant Manage Messages or pin it by hand.
- Used disallowed intents: the two privileged intents aren't switched on in the Developer Portal.
- Login returns "Bad OAuth state" or a Discord error: the redirect URL in the Portal doesn't exactly match
PUBLIC_URLplus/auth/callback.
curl localhost:3311/healthz reports the Discord connection and how many people are in each room.
Beyond one room
Add another file to rooms/ for another server or channel and restart. One bot login serves them all; each room gets its own URL, webhook, snapshot, and membership check. Login is per person, not per room, so someone in two of your servers can walk between rooms without signing in again. The optional ROOMS=a,b line in .env limits which room files load and sets which one / redirects to.
Backdrops live in assets/backdrops/ with descriptions, voice spots, and floors in index.json; overlay images in assets/overlays/ with their own index; word lists in games/wordlists/. Harry chooses by description, so write good ones.
Props are PNGs in assets/props/, 44×44 drawn centered on the head, or 132×132 with the prop already placed in the 3×3 area around it. The ids in PROP_IDS in the shared package are the file names. The scripts folder has tools for pulling props out of original Palace .prp files if you have any.
Enabling Harry (optional)
Harry is an LLM host that speaks from a spot in the picture. He runs as a separate process, harryd, that connects to the room server as an agent and can only use a small server-checked API (say, whisper, who). Nothing he does reaches Discord. Full design in HARRY.md.
- In the server's
.env, addAGENT_TOKENS=harry:<long random token>(openssl rand -hex 24) and restart the server. - Copy
packages/harryd/.env.exampletopackages/harryd/.env: the same token asAGENT_TOKEN, an OpenRouter API key with credits, the room server's WebSocket URL (ws://127.0.0.1:3311when on the same host), and a daily dollar cap. - Optionally name the host role in each room file,
"hostRole": "barkeep". Server admins are hosts regardless. Optionally set"voice": { "name": "Harry", "avatar": "harry", "spot": [322, 45] }. Per-backdrop appearance comes fromassets/backdrops/index.json: each backdrop'svoiceis either{ "mode": "spot", "spot": [x, y] }(a voice from the picture) or{ "mode": "avatar", "spawn": [x, y] }(a movable head drawn fromassets/faces/<avatar>-head.png, 88×88, square). - Run it:
npm run harrydlocally, or on the host installdeploy/harryd.serviceandsudo systemctl enable --now harryd. The deploy script restarts it with the server.
Controls from Discord, hosts only: /harry status, /harry off (drops him and refuses him until /harry on), /harry log (his last hour), /harry reset (room back to normal). In the room, a host saying Harry, stop clears his balloons and tells him to stand down. HARRY_ENABLED=0 on the server refuses agents entirely. Every call he makes is written to the agent_log table.
Before you publish a fork
Secrets live only in files that are ignored by git: .env, packages/harryd/.env, rooms/*.json, deploy/local.env, and data/. Each has an .example or .sample beside it. Before pushing to a public remote, run scripts/secret-scan.sh: it greps the tracked tree and the whole history for API keys, bot tokens, private keys, Discord ids, and per-install files, and exits non-zero if it finds any. Edit its hostname pattern for your own domain.