Halbreven
Replaces the group chat for recreational sports: ratings, balanced teams, and reliability you can see.

- migrations, every table secured by RLS
- 43
- sports across 10 playing surfaces
- 21
- read policies on the ratings table
- 0
The problem
Amateur sports groups organise on a chat thread that forgets everything — who said they were coming, who actually turned up, and who drops out at 9pm on a Saturday. Teams get picked by whoever shouts loudest, which produces the same lopsided sides every week. The three things that would fix it are the three things a chat app structurally cannot do: anonymous peer ratings that accumulate, automatic team balancing driven by them, and a reputation layer that makes reliability visible.
What I built
An Expo application targeting iOS, Android and web from one codebase, over Supabase Postgres. The security model is row-level security rather than application code: every group-scoped table carries a group id, and every policy resolves through a security-definer helper instead of reading the table it guards. Writes that must be atomic across tables are stored procedures, and the tables involved have no insert policy at all — which makes a half-created group, invisible to its own owner, unrepresentable rather than merely unlikely.
Anonymity is enforced by structure, not by policy logic. The ratings table has row-level security enabled and no policies whatsoever, so no client can read a row under any circumstances. Ratings are sampled, submitted and folded into player statistics with Bayesian shrinkage inside a single transaction on the server. The stated reason is that if one player can see how a teammate rated them, the app has manufactured a real argument out of a Sunday kickabout.
Around that: a team balancer that seeds by position then refines with bounded pairwise swaps, a pitch view that draws the teamsheet on the actual playing surface across 21 sports and 10 surfaces, an append-only activity log with no write policies where triggers narrate every change, and a full in-memory demo backend so the app runs on built-in data with zero setup.
The hard part
Deriving the waiting list instead of storing it
A six-a-side match holds twelve; the thirteenth and fourteenth wait, and a drop-out promotes the next in line. The obvious implementation stores a queue position per player — which then needs a job to run on every withdrawal, a transaction to renumber everyone behind the person who left, and a lock so two simultaneous sign-ups do not both claim the thirteenth shirt.
The implementation is one nullable integer column holding the squad size. Everything else is derived: order by sign-up time, take the first N. Promotion becomes a consequence of the read rather than a write that has to fire, so there is no job to run and no stored position that can drift out of agreement with reality. The race disappears because the resolution is the timestamps that already exist. Re-joining after dropping out lands at the back for free, because the new timestamp is later — behaviour that would otherwise have to be written deliberately. Null means uncapped.
Outcome
- 43 database migrations, with row-level security on every table and column-level rules enforced by triggers where policies cannot express them.
- Ratings anonymity guaranteed structurally — security enabled, zero policies — rather than by a rule that a later change could weaken.
- 21 sports across 10 playing surfaces, with per-sport formations drawn on the pitch itself.
- A public progress page that publishes what is not finished, including that nobody outside the author has used it yet.
Screens

Five sports on one screen. The waiting badges are derived, not stored: whoever said yes after the cap is the queue.

The waiting list as the product states it. There is no stored position to renumber; the read decides who is in.

Twelve players, two sides, one pitch. Formations are per sport; the balancer seeds by position and refines by bounded swaps.

Anonymity as the product promises it. The table behind this screen has row-level security enabled and no policies at all, so no client can read a rating.

Reliability you can see: turnout per match, and who never misses a Sunday. A no-show costs trust; turning up earns it back slowly.
Want something like this built? I take on a small number of projects at a time.