Skip to content
UA.
All projects
LiveFull-stackJun 2026 — Present

TTRPG Companion

A real-time table for a tabletop campaign — and a server nobody has to trust.

A character sheet and the pixel-art battle arena, side by side on two phones.
My role
Solo developer — the API, the real-time hub, the rules engine and the client.
Status
Live

The problem

Running a tabletop campaign means tracking hit points, initiative order, inventory, status effects and relationships across paper sheets and a group chat. When part of the table is remote, all of that shared state lives in one person's head — and so does every dice roll, which everyone else simply has to believe.

What I built

A turn-based companion for the campaign I run with friends. One Expo client — iOS, Android and, through react-native-web, a plain browser link — talks to a .NET 8 API over a SignalR hub, so every player sees the same sheets, the same initiative order and the same combat turn as it happens. The rules live on the server: around forty domain services covering dice, combat phases, traits, status effects, triggered reactions, arena modifiers and a day-and-rest cycle.

What it looks like

  • The battle arena: pixel-art sprites in a back street, with the turn order and the attack-roll panel below.
    A contested attack in progress. The attacker enters their roll, the defence opens on the other player's device, and the server decides the outcome.
  • Character selection: a pixel-art portrait with health, strength, agility, honour and money, and a backstory.
    Three playable characters, each with stats and the part of the story they walked in from.
  • A character sheet with a health bar, a connection badge, and sections for stats, abilities, items, conditions, story, wallet and relationships.
    The sheet every player sees update live — the badge in the corner is the SignalR connection, not decoration.

Architecture

  1. Client

    One codebase for iOS, Android and the browser. The web build is the one that gets used: a link in the group chat beats asking five people to install something.

    • Expo
    • React Native
    • expo-router
    • react-native-web
  2. Real-time hub

    Every action at the table — joining, rolling, attacking, ending a turn — is a hub method broadcast to the room. Rejoining re-reads the current state instead of replaying what was missed.

    • SignalR
    • .NET 8
  3. Rules engine

    Dice, combat phases, traits, status effects, triggered reactions and arena modifiers. Each rule is one service, so a new mechanic is a new file rather than another branch in an existing one.

    • C#
    • Domain services
  4. Persistence

    Rooms, characters, initiative and the live combat turn are rows, not memory. SQLite locally and PostgreSQL in deployment, behind the same context.

    • EF Core 8
    • PostgreSQL
    • SQLite

Decisions & trade-offs

  • The server rolls the dice

    A client that generates its own numbers is a client that can be persuaded to generate better ones. The raw d20 is rolled server-side, and the modifiers from stats, equipment and relationships are applied there too — so the result that reaches the table is one nobody could have edited on the way.

  • But a real die still counts

    People at a physical table are not going to put their dice away, and an app that tells them to is an app they close. Alongside the server roll there is a path for submitting a number you rolled yourself; the same rules apply to it. The anti-cheat protects the remote players without disciplining the ones in the room.

  • Connections in memory, the game in the database

    SignalR connections are disposable — a phone sleeps, a tunnel drops, a deploy restarts the process. Treating them as the source of truth would mean a lost connection is a lost session. The room state is persisted instead, so reconnecting is a read rather than a recovery.

  • What a game master may do is a server rule

    Spectator, player and director join through separate hub methods with their own access level, instead of the client hiding buttons it hopes nobody finds. Authority that only exists in the UI is authority anyone can grant themselves with developer tools.

Outcome

Friends join from a link with nothing installed, the session survives a mid-game deploy, and no one has to take anyone's word for a roll.