Problem finding a rule mid-game meant Google and a pile of tabs, and as a new player, it was a lot to take in.
My role all of it, solo. Design, frontend, backend, database and deployment.
Where it is now live and used by my D&D group, with live tables the DM runs, about 1,900 frontend tests, and CI on every push.
Key decisions rules data through my own backend, private data guarded by row-level security, and enemy stats checked inside the database so players never receive them.
Finding rules during a game was a struggle. There was a fan wiki we used that had everything we needed, but it was hard to navigate and slow to find anything in the middle of a session. Mostly, Google was the middleman: search, open a tab, then another, then figure out which one actually answered the question.
As a new player, it was a lot to take in, and the rules lived in one place while my character sheet lived somewhere else.
That's the whole reason I started QuestKeeper.
Who had this problem
Me, and the friends I play with. QuestKeeper was built around our table, and we use it during our games. That's the whole user base. No inflated numbers here.
What I first thought the solution was
A faster reference. The first version was a school final project: a search page and browsable lists of races, classes, spells and backgrounds, with the browser calling the public D&D 5e SRD API directly.
It didn't have character sheets, accounts or anything for running a game. Those came later.
What I built
v0.1 · Jul 2026 · It technically worked
The school project: search plus reference pages, calling the SRD API straight from the browser, hosted on GitHub Pages.
v0.2 · Aug 2026 · Same face, new skeleton
The overall interface stayed the same. Underneath, I moved the SRD calls behind my own Express backend, merged the frontend and backend repos into one monorepo with both histories intact, and deployed to Render.
v0.5 · Sep 2026 · Getting somewhere
Character creation, a full character sheet and leveling, saved in the browser's local storage. The sheet was one long page.
v1.0 · Oct 2026 · Now we're talking
A full redesign, a tabbed sheet, accounts and saved characters in Supabase, guided beginner-friendly character creation, and live tables where the DM runs combat for the group.
v1.5 · Oct 2026 · Pull up a chair
A cozy tavern homepage, a site-wide redesign, and a DM screen split into Combat, Story, Players and Prep tabs. Tables got campaigns, recaps and secret messages, search learned to work by meaning, and the server stopped napping between game nights.
What went wrong
Search made you take an extra step.
The first search sent you to a results page with an empty "Select an item" panel, and nothing useful showed until you clicked. The routing improved in August. The empty panel only fully went away in October, after I saw it again in a portfolio screenshot.
Only one background showed up, and I thought I'd broken it.
Character creation offered exactly one background: Acolyte. I was sure it was a filtering bug. I traced the data path, and QuestKeeper wasn't dropping anything: the 2014 SRD only includes Acolyte. That turned a "bug fix" into a content and licensing decision. The Backgrounds page now also shows the 2024 SRD backgrounds, labeled by edition. Guided character creation still uses the 2014 list; how to bring 2024 backgrounds into creation is an open decision on the roadmap.
The character sheet became one endless page.
Every feature added another section: attacks, spells, resources, equipment, feats, backstory, companion, notes. At level 1 the sheet was 3,073 pixels tall, and a Fighter still scrolled past spellcasting.
What changed
Two data paths
Problem Public rules data and private player data need different things.
Decision Rules requests go through my Express backend to the SRD API. Accounts, characters and tables go straight to Supabase.
Why The backend gives the public data one place to add validation, caching and source information later. Private data is protected where it's stored: row-level security gives each player access to their own characters. The table-sharing policy grants the DM read access; it does not grant editing access.
A combat header and tabs instead of one long sheet
Problem The single sheet was 3,073px at level 1, and sections that didn't apply still had to be scrolled past.
Decision A persistent combat header, six tabs, and a Skills sidebar visible from any tab.
Why The combat numbers stay in view while you move between shorter, focused sections, instead of scrolling one 3,073px page mid-fight. A tab that doesn't apply, like Spells for a non-caster, isn't shown at all, which was simpler than show/hide toggles for every section. On desktop the Skills sidebar stays visible from any tab; on phones the tabs wrap into a grid and a Stats/Skills toggle takes the sidebar's place.
Homebrew-friendly editing
Problem Guided creation works from SRD data, but real characters pick up things the SRD doesn't have: homebrew items, DM-granted resources, feats from other books.
Decision Every core stat is directly editable, and every list accepts freeform entries.
Why Characters that are actually being played change over time, so every stat needs to stay editable as the character changes, not just at creation. One shared editable-list component keeps add, edit and delete consistent across every section.
What's next
Choosing tool and instrument proficiencies during character creation
Sorting, filtering and favorites on long sheet lists, plus a Beginner Mode toggle to hide hints
Caching, timeouts and validation on the backend, and search that still works when one category fails
More automated tests: the database rules, and wider backend coverage
AI showed up after all, but only where it earns its spot: search by meaning, and a first-draft recap built from the notes the table shares. Private notes never go in, and it only runs when someone asks.
Part 2 · The receipts
Architecture
React → Express → D&D 5e SRD API (2014 and 2024) → a consistent response wrapper
React 19, React Router and Vite on the frontend, with plain CSS and design tokens
Node and Express as a thin layer in front of the SRD API
Supabase for auth, Postgres with row-level security, Realtime for live tables, and Storage for portraits
An npm workspaces monorepo
The Claude API for search by meaning and recap drafts, called only from the backend so the key never reaches a browser
Tests and CI
About 1,900 frontend tests in Vitest and React Testing Library, covering the game rules and the sheet, creation, table and Messages screens.
GitHub Actions runs lint, tests and a build on every push and pull request (since Sep 2026).
Backend tests cover the reference-data mappers, the spells endpoint and AI search. The full DM-and-player run is still a written, hands-on test.
Keeping DM secrets secret
Enemy HP and AC live in a table only the table's DM can read.
Attack rolls are checked by a database function that returns the result, not the AC.
The project README records local checks of the database functions using DM, player and outsider identities. Those checks are not currently included as automated tests in the repository.
Phone sheet layout fixed the same day it was spotted, with before and after screenshots
No full accessibility audit yet.
Content and licensing
QuestKeeper uses only SRD content released for reuse. A page being public doesn't mean it can be copied. That's why the fan wiki isn't used as a data source.