All projects
Shipped · Active · since July 2026 ·
next.jspostgresdrizzle

chicker

A mobile-first foosball tracker for a friend group - log a match goal by goal, then find out what it meant.


chicker

chicker

Settle it on the table, then check the record.

chicker is a foosball tracker for a friend group. You log a match goal by goal while you play - one tap per goal, phone in one hand next to the table - and afterwards it tells you what it meant: who won, what it unlocked, a replay of the ball, and a record that splits by opponent, end of the table, table and ball. On top sits a full game layer: 11 badge ladders, 982 achievements, levels, a currency, a shop for Chicky the mascot, and a casino.


What it is

chicker records matches: first to 10, in 1v1, 2v2 or the lopsided 1v2 / 2v1 - always shown from your side, so the player who took on two sees “1v2” and their opponents see “2v1”.

Every goal records who scored, which rod and which figure it came off, and the kind - normal, keeper, throw-in, own goal, or own goal from the throw-in. A keeper goal counts double, so 11 is a possible final score, and own goals credit the other side. The things that aren’t goals get logged too: balls off the table, dead balls, spins.

Around the match, chicker tracks the context that actually decides foosball arguments:

  • Sides - which end of the table you stood at, so you can find out whether you’re genuinely better at the blue end.
  • Tables - the tables you play on, with a photo and a location.
  • Balls - the balls you play with, including mid-match exchanges; every goal remembers which ball went in.
  • Guests - opponents without an account can still be played against, scored and tracked.

And it knows foosball folklore: a crawl - 10-0, or losing 11-x on a keeper goal or 10-x on an own goal - sends the loser under the table, and the app says so.


Why it exists

Every foosball table collects the same folklore - who’s actually better, who’s on a run, who only wins at one end. None of it survives contact with evidence, because nobody writes anything down.

chicker exists to make writing it down cheap enough that it happens, and then to make the record worth having:

  • Logging has to be faster than playing. The goal button is the biggest thing on the live screen, nothing destructive sits near it, and the screen stays awake for the whole match.
  • Stats should be a by-product. Everything - win rates, streaks, badges, even the currency - is derived from the goals you logged. There’s nothing extra to maintain.
  • The record should be fun to come back to. Badges, hidden achievements, a mascot that bonks your opponent with a frying pan, and a replay of the winning goal are why people keep logging.

It’s also a deliberately server-shaped project - Postgres, real auth, server actions, concurrent writers - as a counterweight to the local-first mobile apps I usually build.


Core features

FeatureWhat it does
⚽ Live matchA match clock with pause, one big goal button per player, a figure picker for where the goal came off, special events, mid-match ball exchanges and a running timeline. The screen stays on throughout.
🎉 Result sceneWin and Chicky walks in with a pan and bonks the opponents’ avatars; lose and its temper depends on your losing streak, from a sad trudge to a furious pan slam. Followed by the score, goal difference, unlocks, balls earned and level-ups.
🎬 ReplayWatch the ball afterwards: eight rods in true foosball interleaving, every goal animated from the figure that scored it, paced against the real match clock - or step through it event by event.
✏️ EditingAny participant can rewrite a finished match - date, duration, sides, table and the timeline itself. The match records that it was edited and by whom.
⚡ Quick actionsOne-tap shortcuts on Home for the matches you play most, with their own name and table - “Lunch rematch” and you’re playing.
📊 StatsRecord, win rate, streaks and goal difference over your last 100 games (all time is still viewable), split by end, table and ball - plus rivalries, figures, play time, play days and win rate by weekday.
🤝 Head-to-headA record against every friend, with its own drill-down screen.
🪪 Player cardsA collectible trading card per player: headline number, rank, best mode and position, which hand they play with, six key figures and how far off the next rank they are.
🏅 Badges & achievements11 badge ladders you climb (“how many clean sheets”) and 982 one-off achievements (“a throw-in, then one off your own keeper”) - 285 of them hidden until you earn them.
🐣 Balls, levels & shopPlaying pays balls and XP: every match, every goal, a crawl, a clean sheet, every achievement. Spend them on Chicky’s wardrobe - feathers, jerseys, numbers, hats, faces, neck things, pans and auras - or a Mystery Egg with a random item inside. Some items only unlock.
🎰 CasinoFrom level 3: Kick-off, Chicky Slots, Penalty Shootout and Blackjack, with leaderboards for the biggest win, this week’s winnings and the most wagered. Balls only.
👥 Friends & guestsFind people by username, send requests, and add guests for the people who’ll never make an account.
🔐 Sign-inGoogle or username and password.
📱 InstallableA PWA - add it to your home screen and it behaves like a native app.

Technical details

Stack

LayerChoice
FrameworkNext.js 16 (App Router, RSC, server actions)
LanguageTypeScript (strict)
UIReact 19, shadcn/ui on Base UI primitives
StylingTailwind CSS v4 - a token-driven design system with chunky 3D buttons and no soft shadows
DatabasePostgreSQL via the postgres driver
ORMDrizzle ORM + drizzle-kit, committed migrations applied on server start
Authbetter-auth - Google OAuth and username/password
ChartsRecharts
Iconslucide-react
Deploymentchicker.net

Engineering decisions

  • Six audits that restate the contract instead of importing it. audit:achievements, audit:live, audit:replay, audit:match-edit, audit:economy and audit:casino each guard a failure that’s invisible from the outside - nobody can eyeball 982 achievement rules, and a NaN in the replay just draws no ball without throwing. Because each audit restates its rules rather than importing them, it fails when the implementation moves: audit:achievements found 33 pairs of achievements that were the same rule twice, and when the crawl rule changed, audit:match-edit failed on 102 of 500 fuzzed matches before anyone touched it.
  • A pure domain layer. lib/domain imports nothing outside itself - no IO, no React. That’s what lets the audits compile and fuzz the rules standalone. Every read lives in lib/queries, every write in lib/actions.
  • Badges and coins are derived, not stored. Both are reconciled from the match history: coin earnings are ledger rows, one per reason and reference. Edit a match and what it paid is re-counted; delete it and its coins go with it. Badge queries are never windowed, because the reconcile revokes tiers the record no longer earns - a windowed read would delete them.
  • Null means unset, never a default. A match with no side recorded isn’t blue, and a player who hasn’t said which hand they play with isn’t right-handed. Every statistic skips the nulls instead of guessing, because a side statistic built on a default isn’t a statistic.
  • Stored absolute, shown relative. Modes and ends are stored team-A-first and flipped at read time for whoever is looking. Getting that backwards gives the right answer to the wrong half of the table.
  • Guests are real users. A guest is an ordinary user row that can never sign in. Every relation points at a user id, so guests play, score and show up in stats with no special casing - and linking one to a real account later is a repoint, not a migration.
  • Hidden achievements stay hidden on the server. The names and conditions of the 285 hidden achievements are redacted before they leave the server, and achievement ids are permanent primary keys - the module throws at load on a duplicate, so a collision is a failed build rather than a quietly missing award.
  • Concurrent taps serialize. Match writes lock the match row first, wallet spends lock the player’s row so two taps can’t spend the same balls twice, and client-generated ids make a retried tap a no-op instead of a phantom goal. The client never says which team a goal counts for - the server works it out from the scorer and the goal kind with the same function the optimistic preview uses.
  • Faces stay off match reads. Avatars are base64 data URLs, and once made match reads 3-16 MB. Match queries now carry only ids and names; screens that draw faces fetch avatars separately and join by id.
  • An honest casino. Outcomes are drawn server-side with node:crypto, audit:casino checks that every game keeps a house edge, and the dev-only switches that force outcomes are dropped server-side in production.

Get & use chicker

Use it

chicker is live at chicker.net. Sign in with Google or a username and password, add your friends (or guests), and start a match. Add it to your home screen and it behaves like a native app.

Local setup

You’ll need a Postgres database and, for Google sign-in, a Google OAuth client.

git clone https://github.com/elRatto21/chicker
cd chicker
pnpm install

Create .env.local with DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL, GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.

First run

pnpm dev

# Open http://localhost:3000

Migrations apply themselves on server start. Sign in, add a friend, and start your first match.

Uses pnpm for all package management.


chicker - settle it on the table, then check the record.