Bragi SoftwareSoftware Solutions

Personal studio

BragiBytes

Build notes, shipped work, and open conversation.

Back to devblog

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.