Beholden is a fast, self-hosted campaign tracker for tabletop RPGs. It has two distinct interfaces that share a single backend:
- DM App (
web-dm) - for Dungeon Masters to manage campaigns, encounters, combat, NPCs, treasure, notes, and the compendium - Player App (
web-player) - for players to manage their characters, view shared campaign info, and follow along in real time
Everything runs on a single Node.js server with a local SQLite database. No cloud account required.
DM App
- Campaign, adventure, and encounter management
- Live combat tracker with initiative, HP, conditions, spell slots, and legendary actions
- Player roster with HP, AC, conditions, and death saves
- INPCs (in-party NPCs) with full stat tracking
- Native JSON monster, item, spell, and rules compendium
- Treasure and notes per campaign and adventure
- Real-time sync across all connected clients via WebSocket
Player App
- Character creation and management
- Character sheet with stats, HP, AC, speed, and abilities
- Campaign dashboard showing your assigned campaigns
- Real-time updates from the DM
Auth & Multi-user
- JWT-based login with per-user accounts
- Role-based access: Admin, DM, Player
- Admins manage users and campaign memberships
- Players are redirected to the player app automatically; DMs access the full DM interface
- During active combat, players can open an Engaged Enemies drawer showing damaged hostile combatants as Damaged, Bloodied, or Down without exposing exact HP.
beholden/
|- server/ -> Express API, SQLite, WebSocket
|- web-dm/ -> DM React app (Vite)
|- web-player/ -> Player React app (Vite)
|- shared/ -> Shared styles
|- .env -> Local configuration
`- start.bat -> Windows quick-start script
Requires Node.js 18+.
git clone https://github.com/cbgfx/beholden.git
cd beholden
npm installCreate a .env file in the repo root:
BEHOLDEN_SUPPORT=true
BEHOLDEN_RATE_LIMIT_WINDOW_MS=900000
BEHOLDEN_RATE_LIMIT_MAX=5000
WEB_PORT=5173
SERVER_PORT=5174npm run dev- DM app:
http://localhost:5173 - Player app:
http://localhost:5175 - API:
http://localhost:5174
npm run build
npm startThe server serves both apps and the API from a single port (default 5174):
- DM app:
http://localhost:5174/ - Player app:
http://localhost:5174/player/ - API:
http://localhost:5174/api/
Both app footers display the running server version. Beholden checks the version on the
repository's main branch once per day. When a newer version is available, an
administrator can select Update Available to launch update-beholden.bat, which performs
a fast-forward-only pull, installs dependencies, and rebuilds the applications. Restart the
running server after the batch file finishes. Local tracked changes that conflict with an
update are left untouched and cause the pull to stop instead of being overwritten.
On first run a default admin account is created:
Username: admin
Password: admin
Change the password immediately via Admin -> Users, or set BEHOLDEN_ADMIN_USER / BEHOLDEN_ADMIN_PASS in .env before the first run.
| Variable | Default | Description |
|---|---|---|
PORT |
5174 |
Server port (Railway injects this automatically) |
SERVER_PORT |
5174 |
Dev-mode server port |
WEB_PORT |
5173 |
Dev-mode DM app port |
WEB_PLAYER_PORT |
5175 |
Dev-mode player app port |
BEHOLDEN_DATA_DIR |
Platform default | Directory for the SQLite database and uploaded images |
BEHOLDEN_DB_PATH |
<data_dir>/beholden.db |
Override the database file path |
BEHOLDEN_ADMIN_USER |
admin |
Initial admin username (used only on first run) |
BEHOLDEN_ADMIN_PASS |
admin |
Initial admin password (used only on first run) |
BEHOLDEN_JWT_SECRET |
dev secret | Secret for signing JWTs - change this in production |
BEHOLDEN_SUPPORT |
false |
Show a support link in the UI |
BEHOLDEN_RATE_LIMIT_WINDOW_MS |
900000 |
Rate limit window in ms |
BEHOLDEN_RATE_LIMIT_MAX |
5000 |
Max requests per window |
BEHOLDEN_LOG_EGRESS |
- |
Set to true to log large per-request wire egress after compression |
BEHOLDEN_LOG_EGRESS_MIN_BYTES |
1048576 |
Minimum compressed response bytes logged when egress logging is enabled; use 0 temporarily to log every response |
BEHOLDEN_DEBUG |
- | Set to true for verbose server logs |
Beholden is designed to deploy to Railway as three services from the same repo:
| Service | Start command | Domain |
|---|---|---|
server |
npm start |
api.yourapp.com |
web-dm |
npm -w web-dm start |
dm.yourapp.com |
web-player |
npm -w web-player start |
player.yourapp.com |
Set BEHOLDEN_DATA_DIR=/data and mount a Railway volume at /data to persist the database across deploys.
Set VITE_API_ORIGIN on both web services to point at your server's public URL (e.g. https://api.yourapp.com) so the frontend knows where to connect.
Beholden Grand Schema JSON is the strict, portable native format. Editable exports contain one independently manageable category:
- monsters, items, spells, classes, species, backgrounds, and feats;
- deck cards;
- bastion spaces, orders, and facilities.
From Compendium -> Admin, export any loaded category to a Grand Schema JSON file, export all ten categories as one ZIP, or import a single-category document or flat multi-category document. Matching IDs are always replaced.
Legacy XML and conversion-oriented source shapes are not product inputs. Grand Schema entries use explicit structured fields, reject legacy-shaped fields, and never depend on parsing descriptive prose to discover mechanics.
Adventure version 2 files can embed native compendium batches. Their entries are installed before the adventure, allowing a portable adventure to bring its own monsters, items, spells, and other rules content. The compendium is shared across all campaigns.
- Backend: Node.js, Express, better-sqlite3, WebSocket (ws), JWT auth, sharp
- DM Frontend: React, TypeScript, Vite, React Router
- Player Frontend: React, TypeScript, Vite, React Router
- Database: SQLite (single file, no external server)
MIT - free to use, modify, and self-host.