- The main destinations are always on screen: Campaigns, Campaign and Party (for the campaign last visited, from any page) and Account. They are links along the top bar on a wide screen, and a bar along the bottom on a phone or small tablet (900px or less). - Nothing floats over the page on a phone: the dice roller is the last button of the bottom bar, and quick stats sits in the top bar. - The campaign page and the character sheet start with a row of links to their sections. - On a small screen every table is a stack of labelled cards, rows of controls wrap, fields shrink to fit, and things to tap are bigger. - The encounter list is paged, ten to a page, as is the archived list. Moving an encounter still works across pages. - Text boxes with no type given, and native checkboxes and dropdowns, now match the dark theme. Long monster notes and item descriptions wrap inside their table cell. - All packages at version 1.10.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Sunken Reach — D&D 5e Campaign Manager
A web app for a D&D 5e Dungeon Master and their players to run a campaign together: campaign management, a guided dice-roll character builder, live character sheets, a mid-combat "quick stats" panel, weapons/inventory/spells, monsters, and a real-time encounter/combat tracker.
How it works
/docs describes the app as it currently behaves, one subject per file:
| Document | Covers |
|---|---|
docs/CHARACTERS.md |
Building a character, every number on the sheet, levels, money, permissions |
docs/WEAPONS.md |
The weapon pool, weapon levels and how damage grows |
docs/SPELLS.md |
The spell list, who can use which spells, the spell library |
docs/ITEMS.md |
The item pool, character inventory, items for sale |
docs/ENCOUNTERS.md |
Battles and NPC quests, turns, rewards, trade, what players see |
docs/MONSTERS.md |
The monster list and built-in bestiary |
docs/NPCS.md |
Non-player characters, what they sell and the rates they change money at |
CLAUDE_BUILD_PROMPT.md, docs/GAME_RULES_REFERENCE.md and
docs/ROLL_YOUR_CHARACTER.md are the original specifications the first
version was built from. The app has since moved on in places (gear levels,
the builder's story steps, picking a motivation); the documents above are the
current behaviour.
Getting around
The same four destinations are always on screen: Campaigns (the list), Campaign and Party (for the campaign you were last in, from any page, including a character sheet or an encounter) and Account.
- On a wide screen they are links along the top bar, and the dice roller floats at the bottom left.
- On a phone or small tablet (900px wide or less) they are a bar along the bottom of the screen, with the dice roller as its last button. Nothing floats over the page: on a character sheet the ⚡ quick stats button sits in the top bar.
- The campaign page and the character sheet are long, so each starts with a row of section links (Characters, Encounters, NPCs, Monsters...) that jump straight to that part of the page.
- On a small screen every table (weapons, items, spells, monsters, the party overview) is shown as a stack of cards, one per row, with each figure labelled, and rows of controls wrap rather than run off the side.
Stack
- Backend: Node.js + Express, JavaScript, Prisma ORM, PostgreSQL, Socket.io, JWT auth (bcrypt + jsonwebtoken), zod validation.
- Frontend: React + Vite + TypeScript, React Router, TanStack Query, socket.io-client.
Prerequisites
- Node.js 18+ (tested on Node 22)
- A running PostgreSQL instance (local install, Docker, or a hosted service like Neon/Supabase)
First-time setup
npm install # installs both workspaces (backend + frontend)
# Backend: point at your Postgres instance and run migrations
cd backend
cp .env.example .env # edit DATABASE_URL and JWT_SECRET
npx prisma migrate dev --name init
npx prisma db seed # adds the admin account + the Aldenfell campaign if missing (see output for login/invite code)
# Frontend
cd ../frontend
cp .env.example .env # defaults to http://localhost:4000, adjust if needed
Running in dev
In two terminals, from the repo root:
npm run dev:backend # Express + Socket.io on http://localhost:4000
npm run dev:frontend # Vite dev server, prints its own port (5173 by default)
Open the frontend URL, register an account (or log in with the seeded admin account — see the seed script's console output for credentials and invite code), and start playing.
The seed only adds: it never touches an existing account or campaign, so it is
safe to run on a server with real data. It skips the campaign if the admin
already has an active "Aldenfell"; archive or delete that one in the app
(Campaign Settings) and run it again for a fresh copy. SEED_ADMIN_EMAIL,
SEED_ADMIN_PASSWORD and SEED_ADMIN_NAME set the account it creates.
npx prisma db seed -- --reset wipes every user and campaign first.
A second campaign, Hollowcrown: The Rift Between Worlds, has its own seed and is not part of the default one. It follows the same rules (it only adds, and skips if the admin already has an active Hollowcrown):
cd backend
npm run seed:hollowcrown
On a server, after pulling a new version, run npm run deploy in backend
and then restart the backend. It applies new migrations and regenerates
the Prisma client; without the second half the server keeps running with the
old data model and pages that use anything new fail to load (for example
"Could not load this encounter"). Change your password from the account page
("Account" in the navigation).
Tests
cd backend
npm test
The calculation engine (src/game/calculations.js) is fully unit tested and
runs with no external dependencies. The route-level integration tests in
src/routes/api.integration.test.js need a real, migrated database reachable
via DATABASE_URL — they skip themselves gracefully (with a console warning)
if the DB isn't reachable, so npm test still passes in an environment with
no database configured.
Project layout
/backend — Express + Prisma API, Socket.io server
/prisma — schema.prisma, seed.js
/seed-data — reference JSON (weapons seeded per-campaign; species/classes/skills
are ported into src/game/data/*.js and used directly by the
calculation engine, not stored as DB rows)
/src
/game — calculations.js (the ONLY place D&D formulas live) + buildFlow.js
/middleware — auth + campaign/character authorization
/routes — auth, campaigns, characters, encounters, notes
/sockets — Socket.io room wiring
/frontend — React + Vite + TypeScript app
/docs — game rules & character-builder specs (reference material)