Back to work
ENPL

Case study · product, design and engineering

Rolling Dices: from RPG companion to realtime virtual tabletop.

From October 2022 to April 2024, I led product, research, UX and architecture — and co-developed — a realtime RPG platform. Three engineers, only one full-time, shipped a playable browser VTT, tested it with 100+ users and closed it when adoption did not justify further investment.

9 min read

Period
Oct 2022 — Apr 2024
Role
Founder & Lead Engineer
Research
25+ sessions · 100+ users
Outcome
Playable MVP · deliberately closed
March 2024 early-access build, rendered from the original source with representative session data.

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.

Two products solving the same problem from opposite ends

Map-first VTT

Put the physical table on a screen; let each group bring its own rules.

  1. Map
  2. Tokens
  3. Chat
  4. Dice

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.

  1. Characters
  2. Sessions
  3. Dice
  4. Rules
  5. World
  6. Board

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.

How we researched
  • Discord server · voice + app

    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.

  • Bykon · Copernicon · pubs, clubs, parties

    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.

  • One-to-one, screen shared

    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.

  • After sessions · Discord channels

    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.

One research cycle, roughly every two weeks
  1. 01

    Run a session

    A trained GM plays, one of us watches and writes nothing but what happened.

    timestamped notes + recording
  2. 02

    Ask what it felt like

    Fifteen minutes with the table, a longer call with the GM.

    interview notes
  3. 03

    Sort the evidence

    Moments grouped by what they say about the product, not by who said them.

    findings board
  4. 04

    Pick one responsibility

    The single next thing the app should own, argued against the product boundary.

    Figma flow + scope note
  5. 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.

Walk-up product tests at Bykon in Bydgoszcz.
Explaining the product to a local student radio station between walk-up sessions.
The flyer handed out at conventions. The inside was a two-page rules primer written by our co-founder.
A moderated remote playtest: Discord beside the live session.
The primer: what a role-playing game is, how to create a character and how an action roll ends in success, twist or failure.

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.

Five releases, and the share of the table’s bookkeeping each one took over
  1. 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.

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

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

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

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

LOG

Session memory

An append-only history where dialogue, rolls, items, health, resources and location changes shared one timeline.

  • messages
  • roll records
  • story events
  • state changes
CHAR

Character 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
ACTN

Rules-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
PARTY

Campaign 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
WORLD

Exploration

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
BOARD

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

One session cycle and the state each phase wrote
  1. 01

    Enter the world

    The GM activates a location; each player occupies an area and place.

    db_Session + whereabouts
  2. 02

    Surface an event

    A location-aware story card introduces an encounter, hazard or treasure.

    instance_StoryCard
  3. 03

    Build the encounter

    The GM spawns entities, places them on the board and sets initiative.

    instance_Entity + Board
  4. 04

    Resolve actions

    Turns and called rolls combine character rules, position and modifiers.

    turn_order + PendingAction
  5. 05

    Apply consequences

    Health, energy, equipment, resources and history reflect the outcome.

    SessionPlayer + SessionRecord
  6. 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.

A character sheet becoming an executable context
  1. THEME

    Define the vocabulary

    Attributes, skills and formulas live in one rules config.

    engine/themesConfig
  2. BUILD

    Create the character

    Allocate attributes and skills within the theme.

    character_schema
  3. DERIVE

    Calculate capability

    Base values and weighted skills produce movement, vision and more.

    engine/derivedStatistics
  4. EQUIP

    Apply loadout

    Weight, durability and combat modifiers enter the live context.

    instance_PlayerItem
  5. ACT

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

One action spending three scopes of state
  1. TARGET

    Choose recipient

    Heal yourself or another member of the party.

    target SessionPlayer
  2. CALCULATE

    Price the action

    Mental and Medicine modify the base cost per restored health.

    calculateTreatWounds()
  3. PREVIEW

    Expose consequence

    Show health after treatment and medicine remaining.

    derived client state
  4. COMMIT A

    Restore health

    Write the new current_state to the target player.

    instance_SessionPlayer
  5. COMMIT B

    Spend medicine

    Deduct the cost from the shared inventory.

    db_Session.meds
GM resource control. Coins, provisions, medicine and materials belong to the session, not to a character.
Treat Wounds previews the rules inputs, the health result and the shared-resource cost before committing.

03 World model

The session moved through a world, not a folder tree.

Authored content becoming session memory
  1. AUTHOR

    Package the setting

    Location packs group locations and their card pools.

    resource_LocationPack
  2. STRUCTURE

    Model the place

    Locations contain typed areas; areas contain places.

    resource_Location + Area
  3. LOCATE

    Move the party

    Each player carries explicit location, area and place.

    SessionPlayer.whereabouts
  4. ACTIVATE

    Draw into play

    A resource card becomes a session-owned story card.

    instance_StoryCard
  5. REMEMBER

    Resolve and record

    The transient event closes; its consequence remains.

    db_SessionRecord
Exploration / Areas. The game master sees the location hierarchy, discovered places and where each party member is.
Exploration / Active cards. Authored encounters and hazards as live objects inside the session.

04 Encounters and combat

An encounter moved from content deck to tactical state.

A template becoming a position on the board
  1. SELECT

    Choose a template

    Browse enemies and NPCs scoped to the location.

    resource_Entity
  2. SPAWN

    Create runtime state

    Give the entity session ownership and its own HP.

    instance_Entity
  3. PLACE

    Commit position

    Drag the instance from the pool onto a coordinate.

    board_state[x:y]
  4. ORDER

    Establish initiative

    Combine players and entities into a turn queue.

    turn_order + current_round
  5. RESOLVE

    Advance the fight

    Movement, range, vision and results mutate shared state.

    Board + Player + Entity
Encounter staging. The GM picks reusable templates by tactical role before creating live combatants.
Combat in progress. Round three and the initiative queue stay visible beside character and session state.

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
Elevation overlay. The same live grid shows +4 m, 0 m and −2 m regions while preserving occupants and obstacles.
Field management. Four selected squares can receive a semantic object, an elevation, a disabled state or a reset.
Anatomy of one persisted board coordinate
  1. KEY6.8

    Stable row.column address inside board_state.

  2. SPACEelevation: 7

    Neutral level; maps to 0 m.

  3. CONTENTobject_type

    Optional surface, gas, wall or exploration object.

  4. ACTORentity_id

    Optional player or entity occupying the field.

  5. 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
Ruler mode. Ordered nodes become labelled segments and a running total; board objects remain visible as constraints.
A three-node movement measurement
  1. STARTfield 6.4

    Select the moving figurine’s field.

  2. LEG 018.00 m

    Four horizontal edges to the first waypoint.

  3. VIAfield 6.8

    Turn around terrain without replacing the origin.

  4. LEG 024.00 m

    Two vertical edges to the destination.

  5. TOTAL12.00 m

    Compare 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
Damage preview. A torso hit consumes matching armour durability before reducing the character’s health.
Body-zone damage resolution
  1. HITD12 → torso

    Defence context identifies one of four zones.

  2. LOOKUParmour: torso

    Find the equipped item with the same subtype.

  3. ABSORBmin(9, 11) = 9

    Subtract absorbed damage from durability.

  4. APPLY9 − 9 = 0 HP

    Only unabsorbed damage changes health.

  5. EFFECThealth multiplier

    Remaining 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
Party / Inventory. One card exposes both player-facing meaning and the values the rules engine consumes.
Inventory data and gameplay lifecycle
  1. DEFINEresource_Item

    Type, weight, slots, base durability, modifiers.

  2. OWNinstance_PlayerItem

    Player, quantity, equipped flag, current durability.

  3. EQUIPloadout guards

    Two-slot limit; unique armour zone.

  4. USEaction context

    Apply attack or defence; reduce durability.

  5. REPAIRrepair + record

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

A called roll, from game master to shared session history
  1. 01
    Game master

    Call an action

    Choose the player, category, attribute and difficulty.

  2. 02
    Browser engine

    Derive the dice

    Select schemas from the theme and the character context.

    difficulty_roll
  3. 03
    Postgres + Realtime

    Persist a pending action

    The request survives latency and reconnects; Realtime notifies the player.

    instance_PendingAction
  4. 04
    Player

    Roll

    Resolve a contextual action, not a generic die.

    action_roll
  5. 05
    Browser engine

    Evaluate consequences

    Apply modifiers, energy, health, equipment and XP logic.

  6. 06
    Postgres + 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.

The called-roll modal: recipient, attribute and dice schemas, attack/defence context, role-play bonus and a ten-step difficulty scale.

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.

One session model, composed two ways

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.

  1. Session log
  2. Party
  3. Dice
  4. Exploration
  5. Board
  6. Resources

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 creation
GM call
Player roll
Session Log
Story
Mobile, 2023 Base MVP → March 2024 early access
  1. Character setupLive character state & inventory
  2. Standalone roll formsGM → player rules workflow
  3. Static story checklistLocation, place & event state
  4. Companion navigationSession-wide tool belt
  5. No tactical surfaceEntities, board & turn order

Early access · March 2024

Mobile states rendered from the original application on the same representative campaign as the desktop captures.

Party / inventory
Called roll
Tactical board
Session Log
Exploration

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.

System layers and the consistency loop
  1. Browser · installed PWA

    • React views
    • Redux UI state
    • React Query projections
    • TypeScript game engine
  2. Supabase

    • Auth
    • PostgREST
    • SQL RPC
    • Realtime
    • Storage
  3. PostgreSQL

    • Durable records
    • Runtime instances
    • Rules content
    • Read-model functions

Consistency loop

  1. 01Client writes to Postgres
  2. 02Realtime emits the change
  3. 03Clients invalidate the projection
  4. 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.

  1. resource_*

    Rules and content definitions

    Stable, reusable, configured outside a session

    • resource_CharacterClass
    • resource_Entity
    • resource_Item
    • resource_StoryCard
  2. db_*

    Durable domain records

    Long-lived identity and history

    • db_Session
    • db_PlayerCharacter
    • db_SessionRecord
    • db_Profile
  3. instance_*

    Live session instances

    Mutated during play, broadcast through Realtime

    • instance_SessionPlayer
    • instance_PendingAction
    • instance_Entity
    • instance_Board
  4. 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
Full database model · Beta MVP, August 202319 tables · 29 field-level relationships

resource_*Rules and content

db_*Durable records

instance_*Live session state

db_User

  • iduuidPK
  • display_namevarchar

db_Session

  • idvarchar
  • titlevarchar
  • access_codevarchar[8]
  • owner_iduuidFK
  • theme_idvarcharFK
  • current_location_iduuidFK
  • created_attimestamp
  • updated_attimestamp

db_PlayerCharacter

  • iduuid
  • owner_iduuidFK
  • namevarchar
  • portraitimage
  • classvarcharFK

db_PlayerCharacterDescription

  • iduuid
  • player_character_iduuidFK
  • character_description_iduuidFK

db_PlayerCharacterItem

  • iduuid
  • player_character_iduuidFK
  • character_description_iduuidFK

db_PlayerCharacterSkill

  • player_character_iduuidFK
  • skill_iduuidFK

db_SessionRecord

  • idvarchar
  • authoruuidFK
  • player_characteruuidFK
  • session_iduuidFK
  • record_typerecordTypeEnum
  • contentJSON
  • created_attimestamp

instance_PendingAction

  • idvarchar
  • session_iduuidFK
  • recipientuuidFK
  • typetext
  • contentJSON

instance_SessionPlayer

  • iduuid
  • session_iduuidFK
  • player_character_iduuidFK
  • healthJSON
  • moneynumber

instance_PlayerItem

  • iduuid
  • session_player_iduuidFK
  • item_iduuidFK
  • is_equippedboolean
  • quantitynumber

instance_StoryCard

  • iduuid
  • session_iduuidFK
  • story_card_iduuidFK

resource_Theme

  • idvarchar

resource_CharacterClass

  • idvarchar
  • keyvarchar
  • theme_idvarcharFK
  • portrait_stylestext[]
  • character_schemaJSON
  • base_skillsuuid[]
  • base_itemsuuid[]

resource_CharacterDescription

  • iduuid
  • typeenum
  • keyvarchar
  • class_idvarcharFK
  • contentJSON

resource_LocationPack

  • iduuid
  • tkeytext
  • theme_iduuidFK

resource_Location

  • iduuid
  • tkeytext
  • location_pack_iduuidFK
  • cover_imgtext
  • starting_locationboolean

resource_StoryCard

  • iduuid
  • location_iduuidFK
  • tkeytext
  • typetext
  • contentJSON

resource_Item

  • iduuid
  • theme_idvarcharFK
  • typetext
  • tkeytext
  • contentJSON
  • is_equippableboolean

resource_Skill

  • iduuid
  • theme_idvarcharFK
  • typetext
  • tkeytext
  • contentJSON

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.

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

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

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

  1. 01

    Validate behaviour, not enthusiasm

    Warm interviews and engaging tests are not repeated use, switching intent or willingness to pay.

  2. 02

    Sequence ambition

    Every new system made the product more capable — and delayed a sharper test of the core proposition.

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

Next

Need someone who can own the product and the system behind it?

I work across discovery, interface design, application architecture and delivery — and I am comfortable making the difficult product calls as well as the technical ones.