CogForge: D&D DM Workshop

I DM a weekly steampunk campaign, so I built the table we play on. A live combat engine, an AI story co-writer, a map builder, and a player companion everyone opens on their own phone, all synced over websockets. One Express server, one SQLite file, zero subscriptions.

Node.jsExpressSocket.IO ReactTypeScriptSQLite ZodLLM Integration

See it in action

An 18 second tour: Prep, Combat, Story, and World modes running live against the test campaign

The live encounter engine

Combat is where most VTTs fall apart, so CogForge treats it as one unified screen: initiative order, a token-based battle grid with fog of war, the active combatant's full action economy (action / bonus / reaction), spell-effect zones, hazards, conditions, and a combat log. All of it live at once. Damage buttons, custom attacks, advantage/disadvantage rolls, and concentration tracking are one click from the active turn panel.

CogForge live encounter: initiative, battle grid, action economy, and combat log
Round 4 in the Vault of the Riverford: active turn panel, surprise and hazard trackers, turn order forecast, and the live grid

The player companion, live over websockets

Every character gets a private join link (/player/<token>) that opens a full companion app on the player's phone or laptop. It's not a mirror of the DM screen, it's the player's own surface: their sheet, their actions, their inventory, and a live scene view of the battle map where they drag their token to move. Every HP change, condition, and turn advance the DM makes is pushed instantly over Socket.IO. Both apps share the same appearance settings, so the table and the phones all run the same brass-and-ember theme.

The same app reshapes itself for the phone in your hand at the table: bottom tabs, a thumb-sized action bar, and the same live scene.

Watch a hit land on someone else's screen

This is the part I show people first. The clip below is one continuous sequence: the DM applies damage to the wizard, the engine demands a concentration save on the spot, and then we cut to the player's phone — where the next hit lands live. HP drops, and the concentration check shows up on the player's screen with their own roll button. Nobody refreshes anything. The server validates each action over REST, then broadcasts converged state to every connected view.

One sequence, two screens: DM applies damage and rolls the concentration check, then the player's phone takes the next hit live

And here are both ends of the websocket in the same frame. The DM clicks once on the left; the player's HP bar and concentration prompt land on the right in the same instant.

Side by side: every DM action is validated over REST, then broadcast — both screens converge live, no refresh

The spellcasting engine

Spells are first-class data, not text fields. A 106-spell library carries casting time, range, components, duration, school, save ability, damage type, and concentration, ritual, and upcast flags. Casting from the player companion walks slots by level, logs every cast to a cast_log table, and places persistent area effects: a Cloud of Daggers occupies real cells on the battle grid in a spell_zones table until it ends.

Concentration is enforced, not suggested. Casting a second concentration spell drops the first. Taking damage while concentrating triggers a save at DC 10 or half the damage, whichever is higher — and the prompt goes to whoever needs to roll it, on their own screen. Dropping to 0 HP clears concentration automatically, and every success and failure lands in the combat log.

Story tooling built for improvising

The story engine models a campaign as a graph of beats: taken paths, planned branches, and skipped forks. Not a linear outline. When players inevitably go off script, the Improvise button branches an improvised beat off the current one in two clicks, and the AI quick-moves ("next beat", "fork the path") generate where it leads. The AI Arc Builder goes further: describe the story you want, pick a length (3 to 10 beats) and detail depth, and it generates the full arc with linked NPCs and enemies for every beat.

Branching story graph: an active beat with two labeled pending branches
The story graph mid-session: the active beat and two planned forks, with the Improvise button standing by

World, maps, and AI content

The map library holds grid-based battle maps with per-map fog of war, sizes up to 40×30, and a tile editor for building new ones. The same AI pipeline that writes story beats also creates game content on demand: describe "a massive owlbear matriarch protecting her nest" and the generator returns a complete stat block with abilities, attacks, tactics, and loot, straight into the campaign database. Characters, NPCs, enemies, items, and spells can all be created this way mid-session.

Designing a battle map, start to finish

Here's the editor doing real work: a new 50×40 map from the creation wizard, floor filled, walls and a water channel laid down, then brass pipework, gearworks, and iron grates painted in. Tools have single-key shortcuts, every stroke autosaves, and the same map is immediately playable in an encounter — fog of war, lighting, and token movement included.

Building "The Foundry Floor" in the tile editor: fill, walls, water, then the steampunk furniture

The interesting part: an audit-driven redesign

After the feature set grew, the UI had sprawled to 21 tabs. Rather than guess at fixes, I ran three structured audits scoring the product on profitability, feature completeness, and ease of use, then built a ranked roadmap where issues flagged by multiple audits got priority. The result: 21 tabs collapsed into 5 task-based modes with browser-style split tabs, four combat panels merged into one screen, and a guided character creation wizard.

Campaign dashboard: party roster, what's next, recent events, session log
The campaign dashboard that came out of it: party at a glance, the next beat, recent events, and one-click session start

Measured result: the combined audit score rose from a 151 baseline to ~274/300, with feature completeness reaching 93/100. Every change is logged against the roadmap with before/after scores.

Architecture

COGFORGE · SYSTEM TOPOLOGY DM Workbench · React + TypeScript 5 modes · split tabs · combat console · AI tooling Player Companions · one per character tokenized join links · sheet · scene · actions · bag REST mutations live state push Express REST API + Socket.IO broadcast Zod-validated actions in · converged state out · every connected view stays in sync without polling LLM Content Pipeline characters · NPCs · enemies · items spells · story arcs generates to schema SQLite · better-sqlite3 · 50+ tables · single source of truth characters · spells · encounters · combat_log · spell_zones story_beats · beat_edges · maps · factions · whispers undo_history · redo_stack · cast_log · autosave Timestamped auto-backups · mid-combat autosave recovery · undo/redo history

Engineering Decisions

  • Socket.IO for state, REST for actions. Mutations go through validated REST endpoints (Zod schemas); the server broadcasts the resulting state over websockets so every connected view, DM and players alike, converges without polling.
  • Capability-scoped player tokens. Each character's join link is an unguessable token tied to exactly one character. Players can roll, move, cast, and manage their bag, and they see nothing the DM hasn't revealed (fog of war, DM-only secrets, whispers).
  • Rules live on the server, not in the UI. Concentration DCs, spell slot math, defense adjustments (resistance halves, immunity zeroes, vulnerability doubles), and area-effect zones are all computed server-side, so the DM screen and every phone enforce exactly the same game.
  • SQLite as the single source of truth. One file holds the campaign across 50+ tables; an undo/redo history table makes DM mistakes reversible, and autosave recovers a live encounter after a crash mid-combat.
  • AI generates to schema, not to chat. Every LLM feature returns JSON validated against the same schemas the UI uses, so generated content lands in the database as real, editable game objects.