June 1, 2026 · 4 min
Campaign chat that shares the grimoire
We split the table from the board — same Supabase login, auditable rolls, conjures from the deck.
DND Cards had become the DM’s actual workspace — lists, secrets, card art, a VTT layer that reads what’s on the card. Players still lived in Discord for the session. Rolls pasted as text. Initiative in someone’s notes app. HP in a third tab.
We didn’t want a second product with a second login. We wanted chat that treats dice like data and cards like conjures.
One database, two surfaces
dnd.chat shares the Supabase project with dndcards — auth.users, campaigns, cards, characters, campaign_members. Chat gets its own tables: spaces, messages, roll_events, reactions, threads.
Sign in once. Pick a campaign in the tavern. Your character sheet is already there because the grimoire already knew.
Rolls that stick around
/roll 1d20+5 isn’t decoration. Every result lands in roll_events — advantage, macros, attack vs damage metadata. DMs can audit later. Players can’t edit the log after the fact.
We wired 3D dice because the tavern is purple and flat SVG felt wrong. Three.js die shapes per sides count, brand-matched materials, MP3 hits on mobile-safe defaults. Optional narration — prebaked where we could, ElevenLabs for the long tail.
Conjures, not copy-paste
/summon goblin pulls from the campaign deck. Embed buttons proxy to dndcards’ action engine so “Bite +4” in chat matches the VTT resolution panel. Server-side stat filtering: DMs see the statblock; players never get AC, HP, or CR in the bubble.
Combat state rides beside the thread — initiative, condition chips, auto damage on hit, HP sync back to the character row. Open the map when maps matter. Until then, the chat is the table.
What we didn’t ship yet
Voice (LiveKit stub), Discord mirror, native battlemap — all documented in roadmap, none blocking v1. We had a week of Netlify Turbopack ENOENT failures before we pinned webpack for production builds. Boring fix. Reliable deploys.
Session reminders from table@dnd.chat via Resend. Bunny for attachments. Sentry so we hear when the virtualized list clips a dice result — that one happened twice before we fixed the bubble padding.
The loop
Open dnd.chat → same email as the board → tavern → roll → conjure → whisper the rogue. No “create account again.” No trust-me bro math in #general.
I’m still surprised how much of “running a session online” is just auditable rolls in the right channel — the battlemap can wait.