Skip to content
Arrit Gashi
← All work
Near-shippable2026Mobile app

Halbreven

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

Cover art for Halbreven on a near-black ground: the amber and slate mark, the wordmark in a serif, the line Teams, sorted before kick-off, three chips reading Balanced teams, Anonymous ratings and Reliability you can see, and a phone showing the pitch view with twelve named players in formation, Bibs against No bibs.
Cover art, composed from the brand and one real screen: the pitch view from the demo match shown below.
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

  • The Your groups screen: five cards for Tuesday Futsal, Wednesday Handball, Thursday Hoops, Sunday Lot and Sunday Volley, each with its next fixture, venue and a count such as 10 of 10 in or 11 of 16 in. Two carry a waiting badge and one reads You have not answered.

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

  • A scheduled match at The cages: Are you in with I'm in, Maybe and Can't; a notice that the squad is full at 10 and saying yes puts you in the queue; a squad bar reading 10 of 10 with the line drop out and the next to say yes takes your place; ten player avatars; and a Teams card saying the balancer splits them by grade and position.

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

  • The Teams section of a played match: Bibs 6 versus No bibs 6 above a pitch drawn with a goalkeeper at each end and ten outfield players in formation, every marker numbered and named, then the final score 5 to 4 and both team lists.

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

  • The Rate teammates screen for Driton, Bibs, in goal: a number picker from 1 to 10, Skip and Next buttons, a Driton didn't play option, and the line Anonymous, always. Driton sees an average, never a name.

    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.

  • The Sunday Lot stats tab: the season at 8 played, 15 goals and 12 most in; a turnout bar chart from July to August; a Never miss a Sunday leaderboard ranking six players with streak dots and counts from 8 down to 6; and recent results.

    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.