Snapjack
A multiplayer bankroll manager for at-home poker games. iOS native, web companion, event-sourced ledger, and a multiplayer Glicko-2 rating pipeline — built end to end by one engineer.
The shape of it
Snapjack is what I wanted to exist when I started running a regular home game: a single place to track buy-ins, settle the cash, and watch a real rating emerge over a year of Tuesday nights. The product is small. The engineering behind it isn't.
Underneath the iOS and web apps sits a shared TypeScript monorepo — types, Zod schemas, and a typed API client that both surfaces import directly. The server is a Next.js app with roughly 200 API routes, a third of which run the admin tools that keep games clean and ratings honest.
The core is an append-only ledger. Every chip movement is an event; balances, statistics, and ratings are projections. That decision shaped almost everything else.
The high-level picture
Next.js 14 API- NextAuth · Apple + Google + Email
- JWT issuance + validation (jose)
- Guest → registered conversion
- MongoDB session adapter
- Dual-layer route middleware
- Per-user rate limiting
- League-specific authorization
- Subscription gating
- Security headers
RevenueCatsubscription events
Mailgunalerts
Telegram
RevenueCatsubscription events
Next.js 14 API- NextAuth · Apple + Google + Email
- JWT issuance + validation (jose)
- Guest → registered conversion
- MongoDB session adapter
- Dual-layer route middleware
- Per-user rate limiting
- League-specific authorization
- Subscription gating
- Security headers
Mailgunalerts
TelegramThe technical stack
User-facing apps
Two surfaces, one product. Both speak the same shared types.
The render layer
RN on iOS, Next on web. Identical patterns where possible.
The contract
Turborepo + npm workspaces. The model only exists once.
~200 routes
67 of them under /admin — analytics, settlement, moderation.
The engine room
Where Snapjack stops being a CRUD app and starts being a poker app.
Where it runs
Boring, on purpose. Vercel + MongoDB + EAS.
Data flow · one request, end to end
- 01iOS / Web → import shared package → typed api-client call
- 02Hits a Next.js API route → validated by shared Zod schema
- 03Route queries MongoDB via Mongoose → returns typed response
- 04Auth: client → NextAuth → MongoDB adapter → JWT issued
- 05Subscriptions: iOS → RevenueCat → webhook → API → MongoDB
- 06Push: API → Expo Server SDK → user devices
React Query