All work

Game-night scorepad for iOS

Seven days from the first commit to App Review.

Cardzo was in Apple's review queue a week after the first commit. Every game is a markdown file, the scoring engine is pure TypeScript shared by the app and the website, and the server computes nothing.

Cardzo scoresheet for a game of Hearts
Cardzo darts 501 scoring with the number pad
7
days from the first commit to the first App Store review
78
games, each one defined in a markdown file
657
house rules that recalculate the sheet mid match
3,717
lines of engine, shared by the app and the website

Built in three weeks, then shipped every week since.

  1. First commit, and the engine works the same day

    Twelve scorers behind one registry, a pure score function, undo and redo, and code generated from 70 game files. 146 tests by the end of the day.

  2. Templates and five languages

    Bidding, scorecard, and target templates joined the rounds template. The interface was localized to Danish, German, Spanish, and French, and the rules for all 70 games were translated over the next day.

  3. Keep Score becomes Cardzo

    The rename, a Next.js website with 193 static pages in English and Danish, and the app's engine running in the browser as a web scorepad. The persist keys still say keep-score on purpose.

  4. 1.0 in App Review

    Build 6, with 74 games and 630 settings, seven days after the first commit. The same day, work started on the optional account layer: Circles, rated matches, and game nights on Supabase.

  5. Approved and on the App Store

    The listing sat ready for a day while the storefront returned 404, because no territory availability had been set. Store copy for 2.0 matched build 21: 77 games, 649 settings.

  6. The download switch goes on

    cardzo.app started linking to the App Store, and 2.1 shipped with designed screenshots.

  7. The launch email form is retired

    It had never been wired to a provider and dropped every address. The app had shipped, so it was deleted rather than fixed.

  8. Game 78: Turtle King

    The catalog reached 78 games and 657 settings, where it stands today.

  9. 2.3: over the air updates

    JavaScript updates through EAS Update with a fingerprint runtime policy, so a fix no longer needs a new review.

  10. Live nights on Supabase Realtime

    Guests can enter scores on a shared night from their own phone.

  11. The first over the air update ships

    A weekly Circle league, delivered without a store release.

  12. 2.4: opt in analytics

    Anonymous, off by default, hosted in the EU. 339 commits on main, all by one person.

The stack

One TypeScript codebase. The phone, the website, and the tests all run the same engine.

App
Expo and React Native
Expo SDK 57, React Native 0.86, React 19 with the React Compiler, TypeScript 6. Four tabs on expo-router. iPhone only.
Engine
Pure TypeScript
28 files and 3,717 lines in src/engine. Twelve scorers, four templates, and a withLegs wrapper that adds legs and sets to any scorer.
Games
Markdown
One file per game. YAML frontmatter defines the sheet, scorer, and settings; the body is the rules. A codegen step turns 78 files into TypeScript.
State
zustand and SQLite
Persisted through expo-sqlite with synchronous writes and no debounce, so killing the app mid entry cannot lose a score.
Ratings
Weng-Lin on device
A Plackett-Luce rating update, inlined. Ratings replay from stored facts; the server never computes one.
Backend
Supabase
Optional. Sign in with Apple, Circles, rated matches, game nights, and Realtime for live nights. Stockholm region, free tier, kept awake by a daily cron.
Website
Next.js 16
Tailwind 4 and remark, on Vercel. English and Danish. Game and house rule totals are computed at build time from the same markdown.
Builds
EAS
EAS Build and Submit with auto incremented build numbers, and EAS Update for over the air releases.
Tests
Jest
74 test files, 43 of them worked examples: a real match, entered and scored, with the expected totals.
Languages
5 in the app
English, Danish, German, Spanish, and French. The rules of all 78 games are translated; the website publishes English and Danish.

Games are data

A game in Cardzo is a markdown file. The frontmatter says which template and scorer it uses, how many players it allows, how it is won, and which settings it exposes, with a key, a type, a default, and a range for each. The body is the rules, and a translation is a second file with only a body.

A codegen step turns those 78 files into one TypeScript module. Adding a game means writing a file, not code, and the website computes its own totals from the same files at build time, which is how the catalog can grow from 70 games on day one to 78 without a number being edited by hand.

The sheet is never stored

The engine has one pure function: score takes the game definition, the settings, the players, and the entries, and returns the sheet. Computed totals are never written anywhere. That is what makes house rules recalculate mid match: flip shoot the moon in Hearts and the same entries produce a different sheet, instantly, with nothing to migrate.

Undo and redo are pure mutations on two stacks, and changing a setting is undoable like any other move. Entries are written to SQLite synchronously, on purpose, so a score is durable the moment it is set.

A backend that computes nothing

The first rule was no accounts and no backend. A week after the first commit the plan narrowed to a promise that still holds: the scorepad works offline forever, and accounts are optional. Supabase stores facts for the social layer, Circles, rated matches, and live nights, and nothing else.

Ratings are replayed on the phone from stored match facts, so the server has no formula to get wrong. The project runs on the free tier in Stockholm, kept awake by a daily cron job from the website.

Next case

Chef Gaston