Case Study
CarmaClouds: Multi-Platform VTT Character Sync Suite
Scale
175,000+
Total lines of code
3
VTT platforms
Chrome + Firefox
Browser targets
20+
Discord commands
50+
D&D 5e edge cases
The Problem
Players who build D&D 5e characters in DiceCloud V2 face a painful workflow: the platform provides rich character management, but when it is time to play in Roll20, Foundry VTT, or Owlbear Rodeo, every stat, spell, and feature has to be re-entered manually. 100+ data points, transcribed by hand, with transcription errors accumulating over sessions.
No tool existed to bridge DiceCloud to any VTT platform. The ecosystem had no interoperability.
Key Features
RollCloud
DiceCloud to Roll20 browser extension (Chrome + Firefox)
Parses DiceCloud character data and populates Roll20 character sheets via the Roll20 DOM. Uses Manifest V3 (Chrome) and Manifest V2 (Firefox) compiled from shared source via ESBuild.
OwlCloud
DiceCloud to Owlbear Rodeo native extension
Translates DiceCloud data to Owlbear Rodeo's native extension API. The most complete integration - Owlbear's extension model is well-documented and stable.
FoundCloud
DiceCloud to Foundry VTT module
Foundry requires embedded Items rather than flat attributes, making the translation more complex than Roll20. D&D 5e class items mapped via the system's item compendium.
CoyoteCloud
Two-way DiceCloud sync in the Coyotes and Candles VTT
The most production-deployed component. CoyoteCloud adds DiceCloud write-back to the Coyotes and Candles VTT - character changes made in the VTT push back to DiceCloud in real time, and vice versa. Used in live campaign sessions every week.
Pip2 Discord Bot
Character data via Discord slash commands
20+ slash commands for querying character data, rolling dice with full modifier context, and managing campaign state from Discord.
Technical Highlights
DiceCloud uses Meteor DDP, not REST
DiceCloud's API is built on the Meteor DDP protocol - a WebSocket-based pub/sub system with no REST interface. I implemented a custom DDP client with request queueing to prevent API rate abuse.
Platform data models are fundamentally incompatible
Roll20 uses flat character attributes. Foundry requires embedded Items with specific schema. Owlbear has its own model. The adapter pattern - a shared parsing stage feeding platform-specific adapters - made adding new platforms incrementally cheap after the first two.
D&D 5e rule engine with 50+ edge cases
Generic stat copying is easy. Accurate math - special abilities, weapon detection, expanded crit ranges, multiclass spell slot calculations - is the actual product. Each edge case is a modular pattern-matcher in the rule engine.
Chrome and Firefox extension APIs diverge fundamentally
Solved by writing to a shared source and running an ESBuild pipeline that outputs both targets. Automated ZIP generation at build time prevents deployment confusion.
Monorepo consolidation
RollCloud, OwlCloud, and FoundCloud were originally separate repos. Consolidating into a single monorepo with shared core logic eliminated drift and made the DiceCloud parsing changes apply everywhere at once.
Screenshots
What I Learned
A sync tool spends its life reconciling other people's data models, and almost every bug worth remembering came from trusting an assumption one of them did not share.
A position in a list is not an identity
The extension kept syncing the wrong character. One code path took characters[0], the oldest ever added; another, in the same popup, separately took the last element and shadowed it. The array is insertion-ordered and updated in place, so neither followed the sheet you actually had open, and the tab stayed stuck on one character no matter which you were looking at. The mirror image of the same bug then turned up in a second adapter. Resolving from the id in the open tab, falling back to the most recently synced, fixed both, and the resolver now lives in one place so they cannot drift apart again.
Shared storage needs scoped writes
Pushing one character to Owlbear cleared everybody's. The push handler wiped the whole storage key on success, and that key is shared with the Roll20, Foundry and Coyotes and Candles tabs. It removes only the character it pushed now. Anything writing to storage another feature also reads has to think in rows, not in keys.
Identity in the row is not identity in the token
Cloud syncs started failing with "new row violates row-level security policy". The row carried a genuine user id, but the JWT arriving at PostgREST resolved to a different one, or to anon, so a WITH CHECK comparing auth.uid() to the row rejected it. The data was right and the credential was not. With RLS, the only identity that counts is the one the token proves, which makes it worth asserting what the token resolves to rather than what the payload claims.