Case study · Content Tool
One edit, every game updated
The Go Game's game content was duplicated everywhere, so one typo had to be fixed in several places. I built the Content Tool, where copies share their content and every edit reaches every game that uses it.
- Client
- The Go Game
- Role
- Lead engineer, largest contributor
- Timeline
- 2022–2026
- Platform
- Web, internal
20,000+activities in the library
1edit to update every copy and live game
45activity types imported from Weve games
The problem
Every event at The Go Game runs on a game: a template of blocks and activities. The old workflow was heavily denormalized, so the same question lived in several places, and a typo fixed in one stayed wrong in the rest.
What I built
The Content Tool is the internal CMS for every game The Go Game runs:
- A shared library. Activities, with their steps and media, are the building blocks. Blocks group activities, and templates arrange activities and blocks.
- AI Assist. Describe a block in a prompt, pick the activity type, number of options and difficulty, and the AI drafts it.
- A rich-text editor for game copy, and importers that bring 45 activity types in from existing Weve games.
How edits reach every copy
The core design is that copies share content instead of duplicating it, and every change finds its way up to the games that use it.
- Shallow copies. Copying a template for an event copies only the template and its join rows: order, positions and the relations between items. The copy points at the same activities, so a fix to an activity is a fix in every copy.
- Copy-on-write. Editing a shared activity inside one template clones it into a copy scoped to that template, linked back to its origin. Custom changes stay local and the library stays clean.
- A change feed through an outbox. A PostgreSQL trigger on every content table writes each change to a notification table and sends
pg_notifywith just the row id. A deduplicated Bull job loads the full row and publishes it on Redis. - Version bumps that climb the graph. Each listener only knows its direct parent and bumps that row's
version. The bump is itself a change, so the trigger fires again and it climbs one level at a time:- step media → step
- step → activity
- activity → every block and template that uses it
- block → every template that uses it
- template → the live game
- Coalesced re-exports. When a template's version changes, it's exported to the live game runtime. There is one export in flight per template plus one trailing pass, so a burst of edits costs at most two exports and the last state always lands.
The sharp edge is fan-out: a shared row touches everything that uses it. When a save once rewrote shared activity rows by accident, the version bumps rippled through every template that used them. The rule since is that saves load only join rows, never the shared activities themselves.
Outcome
Creators fix things once. One edit to an activity reaches every template, every event copy and every live game that uses it, which removed most of the repetitive updates and the maintenance time they cost.
Screens