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:
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.
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
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:
- DM workspace
- player accounts
- 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.
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
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:
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)