Back to Work
Web App December 2025 9 min read

Loresmith - D&D Campaign Management Platform

A comprehensive D&D 5e campaign management SaaS application for Dungeon Masters, featuring encounter tracking, a wiki-style note system with bidirectional linking, NPC relationship mapping, and real-time combat management.

React 19TypeScriptPostgreSQLTanStack StartFull-Stack
Loresmith landing page showing the hero section with campaign management interface preview Loresmith landing page showing the hero section with campaign management interface preview

Background

I’ve been running a D&D campaign for a group of close friends for the last five years. What started as a casual experiment (none of us had played before) turned into something that’s now woven into our lives. We’ve watched characters grow from level 1 nobodies into heroes with complicated histories, shifting allegiances, and inside jokes that only make sense if you were there for them.

Five years of sessions generates a lot of material. Plot threads multiply. NPCs accumulate. The world expands in directions I never planned, usually because the Party makes some crazy decision that I could never predict. At some point, keeping track of it all became a project in itself. I tried multiple solutions but everything was missing something that I needed. So I built one myself.

The Problem

Running a D&D campaign means juggling an absurd amount of information. You’ve got NPCs with complex motivations, locations that connect in unexpected ways, quests that branch and interweave, and combat encounters that need split second decisions. Most Dungeon Masters end up with a patchwork of tools: Google Docs for notes, spreadsheets for NPCs, a physical notebook for session prep, and maybe a random encounter tracker that only half-works.

I’d been running campaigns for years, and the friction was constant. My notes were scattered across platforms and ended up being hard to actually use in a useful way. And during combat, I was flipping between tabs trying to coordinate stat blocks, decisions and orchestrate the battle. The tools that existed were either too simple (just note-taking) or too complex (enterprise-level worldbuilding software with learning curves that took weeks).

What I wanted was a unified workspace that helped me write simple notes where I wanted, but still connect campaign information in a way that’s useful.

Loresmith landing page features Loresmith landing page features

The Approach

I built Loresmith as a modern full-stack application using TanStack Start, which gave me a unified framework for both server and client code. This was a deliberate choice. Instead of building a separate API layer, I could use server functions that feel like regular function calls but execute on the server with full type safety. A second layer to this decision was my desire to learn Tanstack Start. I had been familiar with Tanstack Query and Tanstack Start seemed interesting because of that.

The core architecture centers on a few key patterns that shaped everything else:

Server Functions as RPC

Instead of REST endpoints, I use TanStack Start’s Server Functions for all data operations. Each function has Zod validation on inputs, middleware for authentication, and direct Prisma database access. The result is fully typed from database to UI, with no serialization surprises.

Campaign-Scoped Data

Every piece of data in Loresmith is scoped to a campaign. The CampaignProvider context makes the active campaign ID available throughout the app, and every server function validates that the requesting user actually owns that campaign. This isolation is enforced at the database level with foreign keys and at the application level with access checks.

React Query for Everything

All data fetching goes through custom hooks built on TanStack Query. I configured it for a 60-second stale time with 5-minute garbage collection, which means the UI feels instant while still staying reasonably fresh. Mutations use optimistic updates: when you delete a note, it disappears immediately while the server call happens in the background.

Dashboard showing session prep hub Dashboard showing session prep hub

The Wiki System

The notes system was the most technically interesting part to build. I wanted wiki-style [[Note Title]] linking that actually works. You can type a link to a note that doesn’t exist yet, and when you create that note, the link just works. More importantly, I wanted backlinks: if Note A links to Note B, then Note B should know about it.

The naive implementation would query all notes to find backlinks on every page load. That’s O(n) for every read, which gets painful fast. Instead, I cache backlinks as a JSON field on each note. When a note is saved, I parse its content for outgoing links, then update the backlinksCache on every note it references. Reads become O(1), and writes are O(m) where m is the number of linked notes (typically small).

Renaming a note triggers a cascade: I find all notes that link to the old title and update their content to reference the new title. It’s not trivial, but it means the wiki genuinely behaves like a wiki.

Notes editor with wikilinks and backlinks panel Notes editor with wikilinks and backlinks panel

Encounter Tracking

Combat tracking in D&D is surprisingly complex. You’ve got initiative order, hit points, armor class, conditions (some permanent, some timed), concentration for spellcasters, death saves, and a constant flow of damage and healing. I built the encounter tracker to handle all of this without slowing down the game.

Conditions were particularly tricky. A condition like “stunned until end of next turn” is basically a timer that needs to resolve at the right moment. I implemented condition instances with duration types (permanent, round-based, turn-based, until-save) and automatic expiration logic. The UI shows when conditions will expire and removes them at the right time.

The entire combat state lives in React state during the encounter, with periodic saves to the database. This keeps the UI responsive during fast-paced combat while ensuring nothing is lost if the browser crashes.

Encounter tracker with initiative order and combatant management Encounter tracker with initiative order and combatant management

NPC Relationships

Campaign worlds have a ton of relationships. Maybe the blacksmith is the brother of the innkeeper, owes money to the local crime boss, and secretly works for the resistance. You need a way to connect all that. So I built a relationship system that captures these connections: parent, rival, employer, ally, enemy, and custom types. Each relationship is directional and can have notes explaining the context.

The world connections graph visualizes these relationships using Three.js, creating a 3D network that shows how story elements connect. It’s genuinely useful for finding narrative threads you may have forgotten about.

NPC management interface NPC management interface

Quest Board

Quests have their own lifecycle: not started, active, completed, failed. I built a Kanban-style board that shows quest status at a glance, with individual objectives that can be checked off independently. Quests link to NPCs, locations, and notes, so everything stays connected. Still not sure how useful this is, and I suspect I’m just predisposed to Kanban because I use it for work too. So this may end up changing.

Quest board with status columns Quest board with status columns

The Rest

Beyond the core features, there’s a soundboard for ambient audio (backed by Cloudflare R2 for file storage), a dice roller with expression parsing, a loot generator with tiered tables, and a compendium that pulls creature stats from the Open5e API.

Landing page showing feature overview Landing page showing feature overview

Technical Highlights

AreaImplementation
FrameworkTanStack Start 1.0 with file-based routing
DatabasePostgreSQL + Prisma ORM with JSON fields for flexible data
AuthClerk with row-level security for data isolation
CachingReact Query with optimistic updates and stale-while-revalidate
EditorTipTap with custom extensions for wikilinks and entity mentions
AnimationsReact Motion with reduced-motion support
DeploymentCloudflare Workers with edge-compatible Prisma

What I Learned

The development process was a lot of learning for me. The backlinks caching approach taught me to think harder about tradeoffs when planning features. Most applications are read-heavy, so optimizing for reads, even at the cost of more complex writes, usually pays off. Also confirmed to me that data that’s expensive to compute but cheap to store should probably be cached.

The encounter tracker works because I spent time understanding how D&D combat actually flows for different use cases, which required me to really understand the different profiles customers could fit. This type of process translated across the board. I made a lot of technical decisions based on product understanding and what I thought would be useful. Is that the best way? Who knows, but it helped me iterate and improve.

Finally, building FOR yourself was very clarifying, for lack of a better word. I’m used to building around vague and sometimes conflicting requests from other stakeholders. With me as the user, I was able to feel the friction directly. Every clunky interaction, every missing feature, every slow page load registers immediately. That feedback loop was really valuable through the process.

Loresmith landing page hero Loresmith landing page hero

Looking Forward

Loresmith is actively in development. Current focus areas include polishing the UI to feel a bit more like a crafted tool for its specific audience, expanding the combat tracker with more automation, and building collaborative features so multiple players can reference campaign information during sessions.

The architecture is hopefully designed to scale: campaign isolation means users don’t affect each other, and the caching strategy keeps the UI responsive even as campaigns grow. There’s potential to add real-time collaboration, mobile support, and deeper integrations with virtual tabletop platforms down the road.


Built with TanStack Start, React 19, PostgreSQL, Prisma, Clerk, TailwindCSS, and deployed on Cloudflare Workers.