The vote, live
Two steps, Top then Flop, then a live count pushed by a graphql-ws subscription on every ballot. The admin can close early. A deadline is closed by an idempotent sweeper on the server, so nobody has to keep the app open.



A React Native app for amateur football squads: play the match, then crown the Top and roast the Flop of the night. Expo on iOS and Android over a GraphQL API on Node and Postgres.
Counted it() blocks in both repositories on 20 Sep 2026: 119 in the Expo app (React Native Testing Library over ViewModels and screens) and 152 in the API (Jest, with repository tests against a real Postgres via Testcontainers).
Interval of voting-session-sweeper.ts on the API. The sweep is idempotent: concurrent sweeps close a session exactly once and the losers get null.
useImageUpload resizes to 512 px on the device and walks down a JPEG quality ladder until the file is under 1 MB, before the presigned PUT to Cloudflare R2.
Flow files in .maestro/: sign up, sign in, create a team, join by code, browse the tabs, copy the code, cast a ballot. Run on an Android emulator in GitHub Actions, advisory only, since CI has no backend to reach.
The vote runs live over a WebSocket subscription. A session closes by admin, by deadline or by unanimity, and a verdict, once written, never changes. Tokens live in the Keychain and Keystore, pictures resize on the phone before a presigned upload, and both repositories ship with tests, Maestro flows and CI.
Two steps, Top then Flop, then a live count pushed by a graphql-ws subscription on every ballot. The admin can close early. A deadline is closed by an idempotent sweeper on the server, so nobody has to keep the app open.



A session that closed without votes in both categories has no verdict. Standings count closed sessions only, on two podiums, best first, so a lopsided tally never leaks into the table.



Join by a five-character code or create a team with a crest. A new match takes the whole roster by default. The code is guessable on purpose, which is why the API rate-limits it per IP.





Session tokens moved from AsyncStorage to the Keychain and Keystore, with a one-time migration so nobody was signed out. Logout clears the credential, the Apollo cache and the socket. Avatars resize on the phone before a presigned upload to R2.




