DEV Community

Cover image for How a Dev.to Challenge Project Turned Into a Full D&D Campaign Tracker
John Munn
John Munn

Posted on

How a Dev.to Challenge Project Turned Into a Full D&D Campaign Tracker

When I first built this project, it was supposed to solve one narrow problem.

After a D&D session I would have a messy pile of notes, half‑remembered NPC names, and a vague promise to "write a recap later."

A week later someone would inevitably ask:

"Wait… who was that NPC again?"

That frustration turned into a small project called Campaign Keeper.

I originally wrote about the first version here:

https://dev.to/tawe/campaign-keeper-a-session-journal-for-tabletop-rpg-groups-13c1

The idea was simple: after a session I wanted to record what happened in a couple of minutes, generate a player‑safe recap, and avoid the continuity problems that show up in long tabletop RPG campaigns.

Since then the project has expanded a lot.

What started as a quick session journal slowly turned into a full campaign operating system.

It is now live as:

https://campaign-tracker.com

And it has grown into something much broader: a campaign management system for long‑running tabletop RPG games, especially Dungeons & Dragons campaigns.

The app now supports:

  • session logs
  • NPC libraries
  • locations
  • factions
  • world events
  • player portals
  • session scheduling
  • RSVPs and email reminders
  • custom in‑world calendars

This post is about how that happened, what changed in the architecture, and the parts that were much harder than they looked at the start.


The original problem

Most campaign management tools expect the DM to do a lot of work before the campaign even begins.

They ask you to:

  • build a full wiki
  • define every location
  • enter every NPC
  • maintain lore documentation

That sounds great in theory.

In practice most DMs are tired after a session and do not want to spend another hour organizing notes.

So the original design goal became:

continuity without homework

I wanted a fast post‑session workflow where I could record:

  • session title and date
  • public highlights
  • DM‑only notes
  • open threads
  • NPC mentions
  • locations visited

From that, the app could generate:

  • a player recap
  • a DM recap
  • an evolving campaign memory

That constraint shaped the entire system.

Even now that the project has expanded, everything still revolves around that post‑session workflow.


What it became

The current version is much closer to a campaign operating system than a session journal.

Core campaign tracking

  • session tracking with public recaps and DM‑only notes
  • global NPC, location, faction, and event libraries
  • campaign‑specific versions of those entities
  • image uploads for portraits and art
  • a global "vault" view across campaigns

Player coordination

  • player portal with invite links
  • session scheduling
  • RSVP links
  • email reminders

Worldbuilding features

  • world events
  • campaign timelines
  • custom in‑world calendars

None of this was originally planned.

The expansion happened because once the session history became useful, the next question was always:

"Can I click into that?"

If an NPC is mentioned in a session, I want their profile.

If a location appears three times, I want its visit history.

If a faction is behind half the campaign, I want to see everything connected to it.

The moment the recap became useful, the world model around it started demanding to exist.

Dashboard View


What most DMs use today

Most tabletop RPG groups track campaigns using tools that were never designed for it.

Common choices include:

  • Notion
  • Obsidian
  • Google Docs
  • World Anvil
  • Kanka

Those tools are powerful, but they also require a lot of manual structure.

Campaign Tracker exists because I wanted something optimized specifically for the workflow of running a campaign.


The biggest product shift: from notes to entities

To make this concrete, the system ended up looking roughly like this:

Campaign
 ├─ Sessions
 │   ├─ public recap
 │   └─ DM notes
 ├─ NPCs
 │   ├─ global profile
 │   └─ campaign state (knowledge, alignment, notes)
 ├─ Locations
 │   ├─ global details
 │   └─ campaign context (visits, timeline)
 ├─ Factions
 │   ├─ global definition
 │   └─ campaign relationships
 └─ Events
     ├─ global description
     └─ campaign timeline placement
Enter fullscreen mode Exit fullscreen mode

NPC Screen

Each entity has a global record and a campaign-specific projection. Sessions then link across all of them, forming the connective tissue of the campaign.

The most important architectural change was realizing campaign data lives on two layers.

Some information is globally true about an entity:

  • an NPC's portrait
  • a faction's name
  • a location's intrinsic details

But other information is campaign‑specific:

  • what players know about an NPC
  • whether a faction is friendly or hostile
  • private DM notes
  • where a location sits in the campaign timeline

That pushed the architecture toward a two‑layer model:

  • a global entity library
  • campaign‑specific junction records

This allowed villains, cities, and factions to be reused across campaigns without duplicating data.

It also made the data model much more complex.

Simple CRUD is easy.

"This NPC exists globally but appears differently in each campaign" is where systems get interesting.


The hardest part: public vs private data

One requirement existed from the beginning:

DM notes must never leak to players.

Once the app expanded beyond session recaps, that rule became system‑wide.

  • sessions have public and private notes
  • NPCs have player knowledge vs DM notes
  • locations have hidden context
  • factions and events also split visibility

Players access the app through a separate portal.

Some pages can also be shared publicly with no login.

That means "do not leak private data" cannot be a UI rule.

It must be a structural rule enforced server‑side.

The player portal is not the DM interface with buttons hidden.

It is a separate surface with different queries and assumptions.

One of the biggest lessons from this project:

If your product has different trust levels, treat them as different products early.


Sharing is easy. Safe sharing is not

The system supports three sharing modes:

  1. DM workspace
  2. player accounts
  3. public share links

Each has different constraints.

The DM needs full access.

Players need access only to campaigns they belong to.

Public links must expose only the intended resource.

That required tokenized invite links, RSVP links, and route‑level access checks.

None of that shows up in screenshots.

But it matters enormously in production.

Session Notes


Things that surprised me

DMs care more about continuity than lore

Most do not want a giant wiki.

They want to remember what happened last session.

Linking entities matters more than rich text

An NPC page becomes powerful when you can see:

  • every session they appeared in
  • the factions they belong to
  • locations connected to them

Small features are rarely small

The in‑world calendar looked like a cosmetic feature.

It became one of the most complex parts of the system.


The feature that caused the most scope growth

Calendar View

The custom in‑world calendar started as a simple idea.

"What if sessions used the world's calendar instead of real dates?"

That turned into:

  • custom months
  • variable month lengths
  • weekday systems
  • reusable calendar definitions
  • custom date pickers
  • timeline rendering

It touches storage, validation, UI rendering, and event queries.

But it also turns the system into a genuine lore timeline for world‑heavy campaigns.


Why I added scheduling

Campaigns do not only fail because people forget lore.

They fail because nobody knows when the next session is happening.

So the system gained:

  • upcoming session scheduling
  • RSVP links
  • attendance tracking
  • email invites
  • reminder emails

Again, the philosophy stayed the same:

remove recurring friction for the DM.


The stack I chose

The app is built with:

  • Next.js App Router
  • TypeScript
  • Firebase Auth
  • Firestore
  • Firebase Admin SDK
  • Resend for transactional email
  • S3‑compatible image storage
  • Bun for local development

The goal was rapid iteration while still supporting real user accounts, server‑side permissions, and production deployment.

Firebase worked well for this because magic‑link auth is simple and Firestore fits document‑shaped campaign data.

But once relationships grow complex, query design and denormalization choices become much more important.


What I would do earlier if I rebuilt it

If I started again, I would:

  • define public/private data boundaries earlier
  • decide sooner which entities are global vs campaign‑specific
  • assume player access will become a separate product surface
  • be cautious about features that change time, permissions, or relationships

I would still start with the recap workflow.

That was the right nucleus for the product because it solved a real repeated pain point.


What I like most about the final product

The thing I am happiest with is that the system still respects the original constraint.

After a session, a DM should be able to:

  • quickly record what happened
  • preserve campaign continuity
  • generate a player‑safe recap

If that stops being true, the rest of the product does not matter.

Maintaining that balance while expanding the tool has been the most interesting part of the project.


Try it

The live app:

https://campaign-tracker.com

The original article that started the project:

https://dev.to/tawe/campaign-keeper-a-session-journal-for-tabletop-rpg-groups-13c1

If you build tools for niche hobbies, this project reinforced something I keep relearning.

Small ideas can grow into real products when they remove a recurring annoyance for a specific community.

Top comments (0)