Home/Projects/Snapjack
iOS App Store·snapjack.app ·Shipped Mar 2024
Snapjack icon

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.

Role
Software engineer
Surfaces
iOS · Web · Admin
Backbone
Next.js · MongoDB · Expo
Status
Live

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

Client · nativeiOSLive · App Store
Expo SDK 52React NativeNativeWindReanimatedSkiaRevenueCat
Client · webWebLive · snapjack.app
Next.js 14React 18TailwindRadix UID3 + Recharts
Client · adminAdminInternal · /admin
Next.js 14React 18TailwindRechartsD3
data hooks
{ shared client + model }
@snapjack/typesDomain types — the model
@snapjack/schemasValidation contract
@snapjack/api-clientTyped HTTP + cache + validation
Zod validates all requests
Inbound OAuthAuth Providers
Apple·Google·Email
Inbound webhookRevenueCatsubscription events
posts to /api
Next.js 14 API
1Request Pipeline — cross-cutting concerns
  • 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
2Core Engines — services/
Gameevent-sourced lifecycle
Settlement7 sub-services
AnalyticsPlayerStats projection
Notifymulti-channel gate
Leaguefeed · stats recalc
Adminoperator surface → own routes
3Route Groups — by domain
Public surfacesuser-facing
Identity & Social
Auth 8 · Users 10 · Invitations 9 · Friendships 7
Game Lifecycle
Games 23 · Poker 4 · Activity 1 · Stats 4 · PlayerStats 1
Money
Settlements 19 · Transactions 6 · Subscription 2
Discovery & UX
Search 2 · Notifications 6 · Analytics 4 · Theme 1
System
Health 1 · Cron 1
Inbound Webhooks
RevenueCat 2
Restricted · Admin Portal67 routes
Operational Overrides
Settle overrides 5 · League ops 4 · Rating tuning 5
Visualizations & KPIs
Dashboards 18 · Retention · Churn · Conversion
Cleanup & Integrity
Debt-network 3 · Friendship 3 · Recalc jobs 5
User Management
Users 8 · Bans 3 · Roles 2
Bulk Operations
Import 4 · Preview 2 · Confirm 1
2Every wire is Zod-validated. Shared schemas drive client ↔ server. Service-specific schemas guard external integrations.
queries
Read layerIndexingprojections · query indexes
PlayerStats projection Game · Session indexes Compound queries
Mongoose
DataMongoDBMongoose · event log + projections
events
Notification Center
pushExpo Server SDKmailMailgunalertsTelegram
deployed surface shared code (compile-time only) external service inbound webhook outbound integration

The technical stack

Layer 01 · Surfaces

User-facing apps

Two surfaces, one product. Both speak the same shared types.

Layer 03 · Shared monorepo

The contract

Turborepo + npm workspaces. The model only exists once.

Packages
@snapjack/types@snapjack/schemas@snapjack/api-client
Layer 04 · API

~200 routes

67 of them under /admin — analytics, settlement, moderation.

Layer 05 · Domain

The engine room

Where Snapjack stops being a CRUD app and starts being a poker app.

Models
GameUserTransactionSettlementPlayerStatsGameSessionFriends
Engines
Event-sourced game stateLedger-based settlementWelford online varianceSharpe ratioMax drawdown
Rating pipeline · Glicko-2
Per-game min-max profit normalization→ Exponential rank scoring→ Weighted combination→ Two-phi-split update
Layer 06 · Infrastructure

Where it runs

Boring, on purpose. Vercel + MongoDB + EAS.

Data flow · one request, end to end

  1. 01iOS / Web import shared package typed api-client call
  2. 02Hits a Next.js API route validated by shared Zod schema
  3. 03Route queries MongoDB via Mongoose returns typed response
  4. 04Auth: client NextAuth MongoDB adapter JWT issued
  5. 05Subscriptions: iOS RevenueCat webhook API MongoDB
  6. 06Push: API Expo Server SDK user devices
Figure 02 · Six layers, one request — the model never leaves the type system.