Dominican dominoes · iPhone, web, iMessage
Capi
Play 1v1 or 2v2 with friends, live on iPhone and the web or turn by turn in iMessage. No account needed.
- Role
- Timeline
- Stack
- Links
By the numbers
1
rules engine for the server, the web, the iPhone app and iMessage
~1,240
lines in the engine, with 0 runtime dependencies
960
seeded endgames checked for legal moves and completion
8
screen widths, from 320 to 1280 px, with no tile overlap after any move
- Client
- API
- Postgres
- Realtime
- Client to API POST /api
/games The move and the state_version last read by the client./[id] /move - API to Postgres Reads the table
- API to Client 409 if state_version changed The client fetches the latest state.
- API apply
Move Uses the same rules engine as the web and iPhone apps: about 1,240 lines and 0 runtime dependencies. - API to Postgres Writes if state_version is unchanged Only the API can write: no anonymous insert or update policies.
- Postgres to Realtime postgres_changes The updated row.
- Realtime to Client Sends the new state to every player
- Client Resync If no updates arrive, the client fetches the table state after 10 seconds, then every 10 seconds for up to 30 minutes.
The idea
Capi brings Dominican dominoes online. Pick a name, share a code or link, and play 1v1 or 2v2 with your people. No account needed.
Version 1.1 added iMessage games, with invitations and turns sent as chat bubbles.
What I built
I wrote the rules in one TypeScript package: about 1,240 lines with no runtime dependencies. The web app, server API and iPhone app all import it, keeping capicúa, tranque, scoring and board layout consistent. Production deals use crypto.getRandomValues; tests can replay any game from a seed.
The web app uses Next.js and Supabase. Postgres stores game state, and only the API can write to it after a migration removed all anonymous insert and update policies. Supabase Realtime delivers moves; inactive tables also refresh every 10 seconds. The iPhone app uses Expo and React Native with the same API. A Swift iMessage extension creates chat bubbles and displays the web game inside Messages.
The hard parts
-
Validating every move
The API validates each move through the engine and saves it only if state_version still matches what the client read. A move based on an older state gets HTTP 409, preventing simultaneous moves from corrupting the table.
-
Recovering a stalled table
In September, a player reported a stalled game caused by a missed realtime update. I added a state refresh after 10 seconds of inactivity, repeated every 10 seconds for up to 30 minutes. The client shares the fetched state through the table’s realtime chat channel, so players still on version 1.0 catch up too. A new test suite plays 960 seeded games and checks that every table has a legal action and finishes.
-
One board on every screen
The domino chain turns at the table edges and has to fit screens from 320 to 1280 px. A property test plays 200 games through the real reducer. After every move, it checks for overlapping tiles at 8 widths and 2 layout tiers.
-
The same table inside Messages
The native Swift extension handles chat bubbles, player identity and seating. It displays playcapi.com in a web view and uses the same engine and API as the app and web, without duplicating the rules. A script message handler reports moves, round endings and game endings to the extension, which turns them into bubbles. Each game uses one Messages session, so new bubbles replace the previous one.
Before and after
| Before | After | Source | |
|---|---|---|---|
| Recovery from a missed realtime update | A missed update could leave a table where nobody could move. | A 10-second resync, a rebroadcast through the table chat and 960 seeded endgames checked in tests. | Commit f5cadf0 and the stall-fix commits of September 30, 2026 |
What’s next
Next release: show your tile designs to other players and preview designs in full before buying. In iMessage, choose a table and a 100- or 200-point game, with two fixes for joining games.
Try it
Shared approaches
Keep rules separate from the interface
The Cap Room, SWARMGEDDON, Anota, Conuco, The Veil · El Velo
Test against real runtimes and data
Keep English and Spanish in sync