Last push Jul 9, 2026 · MIT
celiosforge
A VTT for D&D 5e
TypeScript0 stars0 forks1 open issues
README
# Oldhart — Your character's home between sessions
A narrative-first D&D 5e web app: daily stories in Dunwatch, a living character sheet, and export paths to Roll20, D&D Beyond, and other VTTs. In-app campaigns and a built-in table remain for groups that want everything here.
**Status:** Beta-ready — core flows work end-to-end; some combat automation and persistence features are still expanding.
**License:** [MIT](LICENSE) — contributions welcome via issues and pull requests.
## Quick start
**Requirements:** [Docker](https://www.docker.com/) (for local PostgreSQL), Node 22+
```bash
npm install
cp .env.example .env
npm run dev:all # starts Postgres + API + Vite
```
Open [http://localhost:5173](http://localhost:5173).
`dev:all` runs `npm run db:up` first (Docker Postgres). Data persists in the `postgres-data` volume.
To run frontend and backend separately:
```bash
npm run db:up # PostgreSQL on localhost:5432
npm run server # API + WebSocket on http://localhost:3001
npm run dev # Vite on http://localhost:5173 (proxies /api, /auth, /ws)
```
### Docker (production-style local test)
```bash
cp .env.example .env
docker compose up --build -d
```
Open [http://localhost:3001](http://localhost:3001). Uses the `postgres-data` volume for the database.
### Deploy to Railway
```bash
npm i -g @railway/cli
railway login
railway link
railway add --database postgres
railway variable set SESSION_SECRET="$(uuidgen)" --service oldhart
railway variable set DATABASE_URL='${{Postgres.DATABASE_URL}}' --service oldhart
npm run railway:setup
```
Production domain: **oldhart.com**. On Railway, add the custom domain under the app service, then at your DNS provider add:
| Type | Name | Value |
|------|------|-------|
| CNAME | `@` | `uhs3767y.up.railway.app` |
| TXT | `_railway-verify` | `railway-verify=840a4349983e697ed05b26a2e8eccc38b3448108b7eec3f84ee0667fd27b7e66` |
Some registrars use ALIAS/ANAME instead of CNAME for the apex (`@`). After DNS propagates, Railway issues HTTPS automatically. Set `DISCORD_REDIRECT_URI` to `https://oldhart.com/auth/discord/callback` (and the same URL in the Discord developer portal). Update `FRONTEND_URL` if you use it.
### Environment variables
Copy `.env.example` to `.env` and adjust as needed:
| Variable | Purpose |
|----------|---------|
| `DATABASE_URL` | **Required** — PostgreSQL connection string |
| `DATABASE_SSL` | Set `false` for local Docker Postgres |
| `PORT` | Backend port (default `3001`) |
| `FRONTEND_URL` | Allowed CORS origin (default `http://localhost:5173`) |
| `SESSION_SECRET` | Cookie session signing key — **change in production** |
| `RESEND_API_KEY` | Send password-reset emails (logs to console if unset) |
| `PASSWORD_RESET_FROM` | Sender address for reset emails |
| `DISCORD_CLIENT_ID` / `SECRET` / `REDIRECT_URI` | Optional Discord OAuth |
| `VITE_API_URL` | Frontend API base (defaults to Vite proxy in dev) |
| `VITE_WS_URL` | WebSocket URL (defaults to proxied `/ws` in dev) |
## What's built
### Character creation
- Multi-step wizard: race, class, abilities, background, skills, equipment, spells, feats, review
- SRD data: races (with subraces), classes (with subclasses), backgrounds, spells, feats, equipment
- Ability generation: Point Buy, Standard Array, 4d6 drop lowest, manual entry
- Auto-calculations: modifiers, proficiency, HP, AC, passive Perception, spell slots
- Printable HTML character sheet with print styles
- Account-backed character library (sign in to save characters)
- Level-up flow, shop, respec potion, arena fights for solo testing
### Virtual tabletop (`/table`)
Three scene modes:
| Mode | Who | Editor | Rewards |
|------|-----|--------|---------|
| **Campaign** | GM + players in a campaign | GM only while **editing** a scene layout | XP, gold, persistent saves |
| **Arena** | Solo character trials | None — prebuilt encounter | XP and gold (daily limit) |
| **Creative** | Anyone sandboxing | Full map editor | None — use **Restore Party** to reset HP |
- Grid map with pan, zoom-to-cursor, and fit-to-view
- Map image upload, adjustable grid size (campaign edit + creative)
- Tokens: drag, facing, size, HP, conditions
- Layout tools: fog, walls, dynamic lighting, light sources
- GM vs player view modes
- In-table chat and dice rolling
- Real-time sync over WebSocket (host a room or join by code)
Open creative mode from the home page or `/table?mode=creative`. Campaign players joining live play can move tokens and fight — they do not get the scene editor.
### Combat
- Initiative tracker and turn order
- Movement with reachable-cell highlighting
- Attacks, spells, class features, and inventory items (potions, throwables) from a unified action panel
- Advantage/disadvantage, flanking (DMG), Sneak Attack, critical/fumble VFX
- Enemy AI for NPC turns
- Spell slot and ki tracking; combat resources persist between fights in campaign play
### Campaigns
- Create campaigns, invite players, manage join requests
- Scenes with persistent map layout (tokens, walls, fog settings, lights)
- **Edit mode** — lay out maps, place NPCs, draw walls; save with **Save & Return**
- **Play mode** — run live sessions; save progress with toolbar **Save**; end session when done
- Session notes and XP awards from the campaign page
- Deploy party roster when starting a scene
### Community
- Forum for posts and discussion (categories, voting, search, bug resolved status)
- User profiles
### Homebrew Studio (`/homebrew`)
Create and manage custom content without editing JSON:
| Type | Features |
|------|----------|
| **Spells** | Live preview card, combat automation (attack/save/heal/AoE/upcast/cantrip scaling) |
| **Feats** | Prerequisites, ability requirements, optional combat automation |
| **Species** | Structured traits (darkvision, speed, resistance, advantage, proficiency) |
| **Subclasses** | Per-feature combat automation |
| **Monsters** | Simplified stat block — appears in monster search and VTT spawn |
| **Items** | Weapons, armor, wondrous items — profile library and compendium |
**Library tools:** JSON import/export, **share-by-code packs** (6-character codes, 30-day TTL), enable/disable per entry.
**Storage:** Guests keep homebrew in browser `localStorage`. Signed-in users sync to the server (`homebrew_libraries` JSONB). Profile page includes a **Homebrew library** section with cards for all content types.
## Beta notes
Things to know when testing:
- **Campaign saves:** GMs see an **Unsaved** badge after changes. Use **Save** / **Save & Return** — the scene also **auto-saves every 30 seconds**. Leaving the page with unsaved changes triggers a browser warning.
- **HP sync:** Token HP updates sync to linked character sheets in campaign and arena play; play-mode saves also push HP before persisting the scene.
- **Rests:** GMs in campaign play can run **Short Rest** (+25% HP, refresh ki and short-rest features) or **Long Rest** (full HP and all resources) from the toolbar.
- **Combat resources persist** across combats in campaign play (spell slots, ki, feature uses). Sandbox combats still reset each fight.
- **Spell automation** covers all SRD spells in the data set; edge cases and newer content may still need polish.
- **Conditions** apply to attacks and saving throws in many cases; broader roll-type coverage is still in progress.
- **PDF export** — **Download PDF** on the character page includes the sheet plus any story/bio sections (multi-page).
- **Password reset** — disabled (Discord-only sign-in); `RESEND_API_KEY` is unused in production.
## Routes
| Path | Description |
|------|-------------|
| `/` | Home hub |
| `/create` | Character wizard |
| `/table` | Virtual tabletop |
| `/characters/:characterSlug` | Character sheet & management (`name-xxxx` slug; legacy UUID bookmarks redirect) |
| `/characters/:characterSlug/level-up` | Level up |
| `/characters/:characterSlug/respec` | Respec wizard (after using a potion) |
| `/characters/:characterSlug/shop` | In-game shop |
| `/characters/:characterSlug/arena` | Solo combat trials |
| `/characters/:characterSlug/town` | Dunwatch — daily story strolls, threads, and street fights |
| `/camp?character=:characterSlug` | Personal camp — rests, stash chest |
| `/inn?character=:characterSlug` | The Inn — chat, trades, duels |
| `/campaigns` | Campaign list |
| `/campaigns/:id` | Campaign detail, scenes, sessions |
| `/forum` | Community forum |
| `/login` | Sign in (email/password or Discord) |
| `/homebrew` | Homebrew Studio (redirects to spells tab) |
| `/homebrew/spells` | Homebrew spells library |
| `/homebrew/feats` | Homebrew feats library |
| `/homebrew/races` | Homebrew species library |
| `/homebrew/subclasses` | Homebrew subclasses library |
| `/homebrew/monsters` | Homebrew monsters library |
| `/homebrew/items` | Homebrew items library |
| `/homebrew/*/new` | Create new homebrew entry |
| `/homebrew/*/:id/edit` | Edit homebrew entry |
## Project structure
```
src/
├── components/
│ ├── auth/ # Sign-in, user menu
│ ├── character/ # Inventory, rest, campaign links on sheets
│ ├── sheet/ # Printable character sheet
│ ├── vtt/ # Game board, combat panel, toolbar, tokens
│ ├── wizard/ # Character creation steps
│ └── ui/ # Shared UI primitives
├── data/ # SRD races, classes, spells, monsters, etc.
├── hooks/ # Combat automation, table permissions
├── lib/ # Rules engine, combat, lighting, campaigns, API client
├── pages/ # Route pages
├── stores/ # Zustand state (game, characters, auth, room)
└── types/ # TypeScript interfaces
server/
├── index.ts # Express + WebSocket entry
├── api.ts # REST routes (characters, campaigns, forum)
├── auth.ts # Session auth + Discord OAuth
├── campaigns.ts # Campaign/scene persistence
├── migrations/ # Numbered SQL migrations
└── multiplayer.ts # Room sync over WebSocket
```
See `AGENTS.md` for the original product spec and longer-term roadmap.
## Scripts
| Command | Description |
|---------|-------------|
| `npm run dev` | Vite dev server (frontend only) |
| `npm run server` | Express + WebSocket backend |
| `npm run db:up` | Start local Postgres (Docker) |
| `npm run db:reset` | Wipe and recreate local Postgres |
| `npm run dev:all` | Postgres + API + Vite (recommended) |
| `npm run build` | Production build |
| `npm run start` | Run API + serve `dist/` (after build) |
| `npm run railway:setup` | Railway deploy helper (variables checklist) |
| `docker compose up --build` | Production container on port 3001 |
| `npm run preview` | Preview production build |
| `npm run lint` | Oxlint |
## Roadmap
Near-term (post-beta):
- Vector-quality PDF generation (current export uses HTML capture)
- Multiclassing in the wizard
Longer-term:
- Mobile-friendly VTT layout
---
Built with care for better character creation and smoother sessions at the table.