← All work

QuestKeeper

A full-stack D&D 5e companion: rules reference, character sheets, leveling and live tables for the DM and players, in one place.

  • Role Solo: design, build and deploy
  • Stack React · Express · Supabase · Vitest
  • Status ● Live, in development · used by my D&D group

The 30-second version

  • 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.
Read the full story ↓

Part 1 · The story

What bothered me

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

  1. 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.

    QuestKeeper v0.1 search results for "fireball": two spells in a list, and an empty "Select an Item" panel beside them.
  2. 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.

    QuestKeeper home page in August 2026, with a dark background, a search bar and category cards.
  3. 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.

  4. 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.

    QuestKeeper v1.0 character sheet for a test character: hit points, armor class and ability scores in a header, a Skills sidebar, and tabs for Actions, Spells, Resources, Inventory, Features and Story.
  5. 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.

    QuestKeeper's DM screen with sample data: the party on the left, the initiative order in the middle with a goblin boss's DM-only hit points, and an activity log on the right.

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.

Character creation, step 9 of 11, "Choose a Background": Acolyte is the only option in the list.

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.

A level 1 character sheet arranged as one long page, with stats and skills followed by attacks, spellcasting, resources and equipment.

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.

How this works → Behind the Build

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 → Supabase: Auth, Postgres, Realtime, Storage
Interactive version → Behind the Build

Stack

  • 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.
More detail: Behind the Build

Accessibility

  • Fixed a low-contrast highlight
  • Labeled number inputs
  • Phone sheet layout fixed the same day it was spotted, with before and after screenshots
  • No full accessibility audit yet.
Before: the character sheet on a phone, with ability-score labels and Save badges overlapping each other.After: the same sheet on a phone, with each ability score in its own card and the tabs in a three-by-two grid.

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.