01 · The opening position
Start beside the tabletop, then earn a place at its centre.
Tabletop RPG groups track character sheets, dice, rules, inventory, wounds, notes and position. Most virtual tabletops began with the map. We asked what software could manage without taking control from players or the game master.
We began with a phone companion for physical play, but the same session also had to work remotely. That set the product constraint: one responsive application, useful both beside the table and as the table.
Map-first VTT
Put the physical table on a screen; let each group bring its own rules.
MapTokensChatDice
The rules stay in the players’ heads; the software stays generic.
Rolling Dices
Digitise the group and the rules first; reach the table once the software knows what a turn means.
CharactersSessionsDiceRulesWorldBoard
The map arrived last — by then the application understood the game played on it.
Rolling Dices evolved in the opposite direction to a map-first VTT: it digitised the rules and the session first, and the tabletop last.
My role
- Founded the product and shaped the beginner-focused proposition.
- Led 25+ research and usability sessions with over 100 users.
- Designed the core flows and interface system in Figma.
- Led architecture and co-developed the React and Supabase application; owned scope and sequencing for a three-engineer team.
- Made the call to stop when the evidence did not support further investment.
02 · What research moved to the centre
The product changed as our understanding of the game changed.
The first release organised accounts, characters and invitations but barely touched play. Tests instead exposed the questions that mattered: what should each role see, who starts an action, and which parts of a session must persist?
The shared session replaced the account as the product centre. Its log joined conversation, rolls, story events and state changes; each shipped loop then exposed the next missing responsibility, from character context to resources and shared space.
- 25+research and usability sessions
- 100+players and game masters involved
- 2conventions and a dozen smaller events
Four ways of looking at the same table.
Moderated playtests on Discord
What it changedGroups returned to the log to settle disputes — which is why the Session Log became the spine of the product rather than a chat window.
Walk-up demos at conventions and meetups
What it changedThe first two minutes decided everything. Character creation went from stat allocation to picking story cards because nobody at a stand wanted to do arithmetic.
Task-based usability tests
What it changedCalled rolls needed the GM to start them. Players asked to roll on their own and then did not know what for — the roll request became a GM action with context attached.
GM interviews and community polls
What it changedGMs did not want automation of the fiction — they wanted bookkeeping gone. That boundary is in the evolution section and never moved.
- 01
Run a session
A trained GM plays, one of us watches and writes nothing but what happened.
timestamped notes + recording - 02
Ask what it felt like
Fifteen minutes with the table, a longer call with the GM.
interview notes - 03
Sort the evidence
Moments grouped by what they say about the product, not by who said them.
findings board - 04
Pick one responsibility
The single next thing the app should own, argued against the product boundary.
Figma flow + scope note - 05
Release and schedule the next table
A build small enough to test with the same GMs within two weeks.
release + next session
Three engineers, one full-time — and the people around the table.
The technical team was small and partly part-time. The product worked because a second circle — a co-founder from publishing, friendly game masters and volunteers from the community — supplied what three engineers could not: real tables to watch, and hands to run them.
Technical team
- Lead — product, research, UX, architecturefull-time
Me. Ran research, designed the flows and co-developed the application end to end.
- Fullstack, frontend focus20–30 h / month
Interface work and feature development, capacity following the current workload.
- Fullstack, backend focusleft in 2023
Owned the original custom server. Resigned during 2023, which reshaped the architecture.
Supporting team
- Co-founder — rulebooks and communitypublishing
Edited and prepared TTRPG rulebooks for a Polish publisher; wrote our rules primer, ran social media and organised every playtest with local communities.
- Friendly game mastersseveral
Trained on the app and ran test sessions at conventions, pubs, clubs, online and at physical tables.
- Community volunteersad hoc
A graphic designer, content writers and community ambassadors who pitched in on flyers, lore, social posts and bringing their own groups to playtests.
- Discord communityplayers
The playtesting server: returning parties, bug reports, polls on what to build next.
When the backend developer left in 2023 we chose not to replace the custom server. Iterating on the frontend alone, with Supabase RPCs as a provisional backend, kept the research cadence alive — and became the architecture bet described later.
What research looked like from the outside.
We tested where tabletop players already gathered: online, at conventions and in local clubs.
03 · The expanding boundary
Each release moved the boundary of what counted as the game.
The product progressed from storing information to coordinating people, sharing state, calculating rules and representing space. Research — not a fixed roadmap — set that sequence.
Late 2022
RPG companion
Stored information
- accounts
- characters
- sessions
- friends
Accounts, characters and a launcher organised the group before play. The important object was still the user account.
Spring 2023
Shared session
Coordinated people
- party
- called rolls
- session log
- invitations
Session Log, party, story and called rolls formed the first complete multiplayer loop.
May — Aug 2023
Realtime assistant
Remembered shared state
- presence
- health
- inventory
- locations
- records
The Supabase migration made sessions live shared objects with pending actions, presence and inventory.
Sep — Dec 2023
Rules engine and world
Calculated rules
- attributes
- derived stats
- difficulty
- energy & XP
- party economy
Attributes, skills, equipment and resources turned character sheets into active game objects; Story became Exploration.
Jan — Feb 2024
Virtual tabletop
Represented space
- grid
- initiative
- elevation
- vision
- stealth
Combat grid, entities, initiative, terrain, elevation and vision completed the move to a tactical browser workspace.
held by the people at the table held by the application
One thing never moved: the fiction, and the game master’s authority to rule on it. Every rung was bookkeeping taken off the table — never a decision taken away from the people around it.
More rules removed more bookkeeping but made the product less generic. A configurable theme system was our answer — and the project’s most ambitious bet.
04 · What the app understood
Not a canvas with tokens. A VTT that understood the game.
By early access, Rolling Dices was a connected set of gameplay systems. The value was never one screen — it was that character rules, party resources, narrative progress and spatial combat all changed the same live session.
LOGSession memory
An append-only history where dialogue, rolls, items, health, resources and location changes shared one timeline.
- messages
- roll records
- story events
- state changes
CHARCharacter engine
Attributes and skills fed derived statistics; equipment, perks and passive effects modified what a character could attempt.
- health & energy
- XP & levels
- carry / jump / throw
- movement & vision
ACTNRules-aware actions
A called roll carried actor, attribute, difficulty and attack or defence context; the engine built the dice and evaluated consequences.
- difficulty_roll
- action_roll
- RP bonus
- passive effects
PARTYCampaign economy
Money, provisions, materials and medicine belonged to the party, so resting, healing and repairing all spent from one pool.
- rest & recovery
- wound treatment
- repair
- level-up
WORLDExploration
A Location → Area → Place model with story cards and whereabouts, giving the game master a different view from the players.
- location hierarchy
- whereabouts
- story cards
- GM / player views
BOARDTactical combat
A shared grid with entities, initiative and rounds, where terrain, elevation, walls and surfaces made position mechanically meaningful.
- initiative queue
- rounds & turns
- distance
- elevation
One session cycle
Narrative and tactical play were one loop: exploration produced events, events produced encounters and actions, actions changed characters and resources, and the resulting history shaped what happened next.
- 01
Enter the world
The GM activates a location; each player occupies an area and place.
db_Session + whereabouts - 02
Surface an event
A location-aware story card introduces an encounter, hazard or treasure.
instance_StoryCard - 03
Build the encounter
The GM spawns entities, places them on the board and sets initiative.
instance_Entity + Board - 04
Resolve actions
Turns and called rolls combine character rules, position and modifiers.
turn_order + PendingAction - 05
Apply consequences
Health, energy, equipment, resources and history reflect the outcome.
SessionPlayer + SessionRecord - 06
Prepare again
Rest, medicine and materials convert party inventory into readiness.
db_Session resources
Every desktop capture on this page is the March 2024 React application rendered from its original source with representative session data. Nothing has been redrawn.
01 Character engine
The character sheet was an input to the rules engine.
THEMEDefine the vocabulary
Attributes, skills and formulas live in one rules config.
engine/themesConfigBUILDCreate the character
Allocate attributes and skills within the theme.
character_schemaDERIVECalculate capability
Base values and weighted skills produce movement, vision and more.
engine/derivedStatisticsEQUIPApply loadout
Weight, durability and combat modifiers enter the live context.
instance_PlayerItemACTResolve in context
The called action uses character, equipment and current state.
action_roll
02 Party economy
Recovery turned character skill into a shared economic decision.
TARGETChoose recipient
Heal yourself or another member of the party.
target SessionPlayerCALCULATEPrice the action
Mental and Medicine modify the base cost per restored health.
calculateTreatWounds()PREVIEWExpose consequence
Show health after treatment and medicine remaining.
derived client stateCOMMIT ARestore health
Write the new current_state to the target player.
instance_SessionPlayerCOMMIT BSpend medicine
Deduct the cost from the shared inventory.
db_Session.meds
03 World model
The session moved through a world, not a folder tree.
AUTHORPackage the setting
Location packs group locations and their card pools.
resource_LocationPackSTRUCTUREModel the place
Locations contain typed areas; areas contain places.
resource_Location + AreaLOCATEMove the party
Each player carries explicit location, area and place.
SessionPlayer.whereaboutsACTIVATEDraw into play
A resource card becomes a session-owned story card.
instance_StoryCardREMEMBERResolve and record
The transient event closes; its consequence remains.
db_SessionRecord
04 Encounters and combat
An encounter moved from content deck to tactical state.
SELECTChoose a template
Browse enemies and NPCs scoped to the location.
resource_EntitySPAWNCreate runtime state
Give the entity session ownership and its own HP.
instance_EntityPLACECommit position
Drag the instance from the pool onto a coordinate.
board_state[x:y]ORDEREstablish initiative
Combine players and entities into a turn queue.
turn_order + current_roundRESOLVEAdvance the fight
Movement, range, vision and results mutate shared state.
Board + Player + Entity
Mechanics in detail
Four places where the app took the rules out of the players’ heads.
01 Board as world state
Every square was a stack of spatial facts.
- Address
- row.column → “6.8”
- Elevation
- 15 levels · −14 m to +14 m
- Field
- 2 m grid · 4 m² per square
- Persistence
- board_
state JSON on instance_ Board
KEY6.8Stable row.column address inside board_state.
SPACEelevation: 7Neutral level; maps to 0 m.
CONTENTobject_type Optional surface, gas, wall or exploration object.
ACTORentity_id Optional player or entity occupying the field.
RULEfield_disabled Blocks placement; marks terrain unavailable.
- Exploration
- curio
- poi
- passage
- door
- treasure
- Surfaces
- fire
- ice
- oil
- water
- poison
- Gases
- smoke
- steam
- poison_gas
- explosive_gas
- Obstacles
- wall_small
- wall_medium
- wall_big
- disabled
One field, five orthogonal concerns. The UI composed them visually; the board document synchronised them as one snapshot.
02 Movement and measurement
Movement was negotiated as a route, not guessed from pixels.
- Scale
- 2 m per grid edge
- Diagonal
- √2 × 2 m
- Route
- ordered origin + waypoints
- Input
- mouse drag · touch drag
STARTfield 6.4Select the moving figurine’s field.
LEG 018.00 mFour horizontal edges to the first waypoint.
VIAfield 6.8Turn around terrain without replacing the origin.
LEG 024.00 mTwo vertical edges to the destination.
TOTAL12.00 mCompare the route with the fighter’s movement.
- Character capacity
- Physical
- Dexterity
- combatMovement
- Entity capacity
- base_movement_range
- initiative
- Drop guards
- occupied
- wall
- disabled field
The ruler measured geometry, the engine supplied capacity, the GM kept the final ruling.
03 Body-zone health
The figurine made damage location and armour condition legible.
- Zones
- head · torso · arms · legs
- Hit location
- D12
- Absorption
- zone armour durability first
- Consequence
- remainder → global HP
HITD12 → torsoDefence context identifies one of four zones.
LOOKUParmour: torsoFind the equipped item with the same subtype.
ABSORBmin(9, 11) = 9Subtract absorbed damage from durability.
APPLY9 − 9 = 0 HPOnly unabsorbed damage changes health.
EFFECThealth multiplierRemaining health modifies later rolls.
- Earlier concept
- zone HP
- minor / moderate / critical
- overall condition
- Early access
- global health
- zone target
- zoned armour
- durability
- Figurine encoding
- red = selected
- bright = strong armour
- dim = worn
- black = broken
Body zones as tactical context around a simpler health core.
04 Inventory as rules input
An item was content, runtime state and action context at once.
- Definition
- resource_
Item - Ownership
- instance_
PlayerItem - Constraints
- weight · slots · body zone
- Lifecycle
- equip · use · damage · repair · log
DEFINEresource_Item Type, weight, slots, base durability, modifiers.
OWNinstance_PlayerItem Player, quantity, equipped flag, current durability.
EQUIPloadout guardsTwo-slot limit; unique armour zone.
USEaction contextApply attack or defence; reduce durability.
REPAIRrepair + recordSpend materials; append the change to history.
- Shared definition
- type
- subtype
- rarity
- weight
- price
- base_durability
- Player instance
- quantity
- is_equipped
- durability
- session_player_id
- Rules consumers
- carry capacity
- attack
- defence
- body armour
- repair
The resource/instance split kept authored content reusable while every session damaged and equipped its own copy.
05 · One action, end to end
The action — not the die — became the mechanic.
The called roll holds both the product idea and the architecture. A game master picked a player, attribute and difficulty; the engine derived the dice and persisted a pending action; the player rolled, and health, energy and equipment shaped a result that entered the shared log. Dice stayed the visible resolution, but the application understood enough context to evaluate the action and keep its consequences.
- 01Game master
Call an action
Choose the player, category, attribute and difficulty.
- 02Browser engine
Derive the dice
Select schemas from the theme and the character context.
difficulty_roll - 03Postgres + Realtime
Persist a pending action
The request survives latency and reconnects; Realtime notifies the player.
instance_PendingAction - 04Player
Roll
Resolve a contextual action, not a generic die.
action_roll - 05Browser engine
Evaluate consequences
Apply modifiers, energy, health, equipment and XP logic.
- 06Postgres + Realtime
Commit and broadcast
Update the player, append a record, delete the pending action; every client refetches.
SessionPlayer + SessionRecord
The board represented the current state; the log explained how the party got there. The database was durable state, multiplayer transport and a lightweight workflow queue at once.
06 · Two contexts, one model
One game model, two contexts of use.
Mobile behaved like a tool belt: one focused context at a time, so the phone supported play at a physical table instead of competing with it. Our first assumption had been the opposite — the early product briefly blocked small screens. Phone use then became a core requirement, down to touch drag-and-drop on the combat board.
Desktop stopped behaving like a larger phone and became a workspace: party, Session Log, dice, Exploration and Board visible together, with infrequent operations opening as modal tasks. The domain model never changed; only the composition did.
Desktop · workspace
Four surfaces held open at once, so the game master reads the party, the history, the world and the fight without navigating.
Party
Characters, health, energy, inventory
Session log + dice
Shared history and the roll in progress
Board
Grid, figurines, initiative, terrain
Exploration
Location, places, active story cards
Modal tasks
- call roll
- manage resources
- rest
- add entity
- change location
- draw card
Mobile · one context
Six contexts, exactly one on screen at a time, each sized for a thumb.
Session logPartyDiceExplorationBoardResources
The phone evolved from a set of screens into a session operating system.
The 2023 concepts introduced discrete tools. By March 2024 those tools were projections of one live multiplayer model, with the same characters, actions, world and combat state on mobile and desktop.
Base MVP · Figma, 2023
Unmodified 2023 pre-alpha designs, shown separately from the later product captures.
- Character setup→Live character state & inventory
- Standalone roll forms→GM → player rules workflow
- Static story checklist→Location, place & event state
- Companion navigation→Session-wide tool belt
- No tactical surface→Entities, board & turn order
Early access · March 2024
Mobile states rendered from the original application on the same representative campaign as the desktop captures.
07 · The shape of the system
A database-centric realtime VTT with the game engine in the browser.
Rolling Dices was a React and TypeScript single-page application backed directly by Supabase. Redux held identity and navigation, React Query held server projections, and high-frequency interaction — dice, dragging, selection — stayed local until it needed persisting. A separate engine layer calculated dice, derived statistics, XP, energy and initiative. Supabase replaced an application server, auth service and websocket layer at once: an unusually lean multiplayer architecture for three engineers, only one of them full-time.
The consistency pattern was event, invalidate, refetch. A browser wrote to Postgres, Supabase emitted a change, other clients invalidated the affected projection and refetched authoritative state. Realtime was not polish — it was the consistency model. Postgres also acted as a backend-for-frontend, with SQL functions composing UI-ready read models, while writes still hit several tables directly from the browser.
Browser · installed PWA
- React views
- Redux UI state
- React Query projections
- TypeScript game engine
Supabase
- Auth
- PostgREST
- SQL RPC
- Realtime
- Storage
PostgreSQL
- Durable records
- Runtime instances
- Rules content
- Read-model functions
Consistency loop
- 01Client writes to Postgres
- 02Realtime emits the change
- 03Clients invalidate the projection
- 04Clients refetch authoritative state
Four owners, four update rhythms
Data lived where its lifetime and mutation frequency made sense; the interesting problem was the seams between owners.
Redux Toolkit
Identity, session shell, transient workflow context
- current user
- active session
- navigation
- modal context
TanStack Query
Server-owned projections and cache invalidation
- players
- records
- locations
- entities
Component state
Interaction that must feel immediate
- dice table
- drag position
- field selection
engine/*
Pure transformations between game states
- dice schemas
- derived stats
- XP / energy
- initiative / vision
The schema name told you what kind of time an object lived in.
Each prefix implied ownership, mutation frequency and how the UI should consume it.
resource_*Rules and content definitions
Stable, reusable, configured outside a session
- resource_CharacterClass
- resource_Entity
- resource_Item
- resource_StoryCard
db_*Durable domain records
Long-lived identity and history
- db_Session
- db_PlayerCharacter
- db_SessionRecord
- db_Profile
instance_*Live session instances
Mutated during play, broadcast through Realtime
- instance_SessionPlayer
- instance_PendingAction
- instance_Entity
- instance_Board
engine/*Domain transformations
Stateless TypeScript between input and next state
- dice
- character stats
- effects
- combat
- movement
Postgres as read-model builder
The get_players_for_session RPC assembled a UI-ready player from six sources, keeping join knowledge out of React.
instance_SessionPlayer
↳ db_PlayerCharacter
↳ db_Profile
↳ resource_CharacterClass
↳ get_player_perks()
↳ get_player_items()One channel, eight live aggregates
A session channel listened for table changes. Most events invalidated a projection and refetched; append-only records patched straight into the cache. Queries never went stale on their own — Realtime, not polling, was the refresh contract.
- db_SessionRecord
- db_Session
- instance_SessionPlayer
- instance_PlayerItem
- instance_PendingAction
- instance_Entity
- instance_LocationState
- instance_Board
08 · What we chose to spend
We spent resilience to buy iteration speed.
The May 2023 Supabase migration was the pivotal bet, made with open eyes. A feature could travel from interface to mutation to Postgres to Realtime with no application server in between. For a part-time engineering team chasing a product that still had to prove itself, that leverage was worth more than resilient infrastructure. The bill came due in specific places.
- 01
Velocity vs authority
Running the rules in TypeScript made iteration fast; it also meant a modified client could choose its own outcomes. RLS protected rows, but it could not express a game rule.
- 02
Simple writes vs transactions
Resolving an action updated a player, created a record and deleted a pending action as separate requests. A partial failure left shared state inconsistent.
- 03
One board document vs concurrency
Storing the board as one JSON document made hydration and broadcasting simple — and let two simultaneous edits overwrite unrelated cells, last write wins.
09 · Outcome
A playable product, a deliberate ending, durable lessons.
By early 2024 Rolling Dices held the complete outline of a browser VTT — characters, shared sessions, contextual dice, a persistent log, inventory, health, party resources, exploration, a combat board with initiative, terrain and vision. It could sit on phones around a physical table, or be the tabletop itself.
The breadth was real; the validation was not. We learned a great deal about how people play, but not enough of them were changing established behaviour in a way that justified continued investment. I closed the product in April 2024 rather than confuse technical momentum with market evidence. Founding a product means owning the stopping criteria as well as the vision.
What I carried forward
- 01
Validate behaviour, not enthusiasm
Warm interviews and engaging tests are not repeated use, switching intent or willingness to pay.
- 02
Sequence ambition
Every new system made the product more capable — and delayed a sharper test of the core proposition.
- 03
Stopping is product work
Closing a project the evidence does not support preserves time, attention and trust for the next one.
The early application lived beside the game. The final application tried to contain the game.