fim2
A self-hosted, AI-augmented virtual tabletop for D&D 5e: a Next.js and Socket.io app per campaign, provisioned by a NestJS control plane.
- Status
- Active, private repository (write-up only)
- First commit
- Latest commit
- Next.js 14
- TypeScript
- Socket.io
- Prisma
- PostgreSQL
- NextAuth
- NestJS
- Docker
- Nginx
This repository is private, so this is a write-up of the architecture and decisions rather than a code tour.
The problem
Hosted virtual tabletops are convenient but I wanted to own the data and the table: maps, tokens, combat, chat and dice for my own D&D 5e campaigns, with AI help for the tedious parts (generating characters, items and lore, rules questions) layered on top of an otherwise ordinary VTT.
Key design decisions
One container and one database per campaign. A NestJS control plane provisions each campaign as its own Docker container with its own Postgres database behind a reverse proxy that routes by subdomain. This gives strong isolation and makes one campaign’s failure or upgrade independent of the others. The trade-off is that anything spanning campaigns, such as a shared asset library or carrying a character between campaigns, needs inter-container coordination and is awkward. That limit is documented in the project, and it is the main thing a later rewrite would revisit.
A custom server so HTTP and WebSockets share one process. The app wraps Next.js 14’s request handler in a custom Node server and attaches Socket.io to the same HTTP server. Fast gameplay events (token drags, measurements, previews) are rebroadcast immediately without a database write; committed actions go through API routes, write via Prisma, and then emit an authoritative event. Authorization runs per socket event.
Postgres is the source of truth. There is no in-memory session actor. That keeps the relay simple, but a second independent client (a dedicated player app) would expose limits in the model, so a server-authoritative design is recorded as a deferred decision with the trigger that would force it. A batch of real sync bugs was found and fixed along the way (stale client cache overwriting live state, reconnect recovery, drag-in-progress tokens being clobbered).
Roles and guests. Owner, GM, player and observer roles, plus signed-invite guest play, all feed the same capability checks. Sign-in delegates to the control plane, so a campaign container cannot start in production without it.
AI is a layer, not the product. Providers are pluggable, and an MCP tool surface with GM arbitration for players’ natural-language intents is in progress.
Stack
Next.js 14 (App Router), React 18, TypeScript, Tailwind, Socket.io, Prisma and PostgreSQL, NextAuth, NestJS, Docker and Nginx.
Outcome and what I would change
About 2,300 commits between 2025-09-08 and 2026-10-17. An earlier NestJS and React stack was superseded by the Next.js app and is kept read-only, with a repository check that fails if a branch touches the legacy code without a stated reason. The honest weakness is the dependency on that legacy control plane for identity, and the per-campaign tenancy model. If I did it again I would collapse tenancy into one multi-campaign deployment before building features that need to cross campaigns.