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. Create the Discord application

  1. Go to discord.com/developers/applications and click New Application. Name it whatever your bot should be called.
  2. General Information: copy the Application ID. That is your DISCORD_CLIENT_ID.
  3. OAuth2: copy (or reset) the Client Secret into DISCORD_CLIENT_SECRET. Under Redirects add https://bar.example.com/auth/callback for production and http://localhost:5173/auth/callback for local development. Discord only accepts callbacks that match this list exactly.
  4. 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:

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.

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.

  1. In the server's .env, add AGENT_TOKENS=harry:<long random token> (openssl rand -hex 24) and restart the server.
  2. Copy packages/harryd/.env.example to packages/harryd/.env: the same token as AGENT_TOKEN, an OpenRouter API key with credits, the room server's WebSocket URL (ws://127.0.0.1:3311 when on the same host), and a daily dollar cap.
  3. 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 from assets/backdrops/index.json: each backdrop's voice is either { "mode": "spot", "spot": [x, y] } (a voice from the picture) or { "mode": "avatar", "spawn": [x, y] } (a movable head drawn from assets/faces/<avatar>-head.png, 88×88, square).
  4. Run it: npm run harryd locally, or on the host install deploy/harryd.service and sudo 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.