logo falcao Dev
.../projects

Buvio

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.

Role
Sole technical owner
Project name
Buvio

Frontend

  • React Native
  • Expo
  • Reanimated
  • Apollo Client

Backend

  • GraphQL
  • Node.js
  • Express
  • PostgreSQL
  • Kysely Query Builder
  • Firebase Auth

Tooling

  • TypeScript
  • Maestro
  • Sentry
  • GitHub Actions CI/CD
  • Docker
271
Automated tests across app and API

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).

10 s
Deadline sweep interval

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.

< 1 MB
Every avatar and crest upload

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.

7
Maestro flows over the real journeys

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 build

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.

The walkthrough

4 sections
01 / 04

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.

Buvio ballot, step one of two: a grid of teammates asking who was the Top, with one selectedBuvio live vote screen with a countdown, three of eight ballots cast and the running tally per playerBuvio live tally with Top and Flop bars per player and the admin button to close the vote
02 / 04

The verdict and the memory

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.

Buvio verdict screen naming the Top of the night in a gold medallion and the Flop below it, with the final tallyBuvio match history with counts of matches, verdicts and open votes, and cards tagged upcoming, vote open or finishedBuvio standings with a personal season summary and a podium of the players with the most Tops
03 / 04

Matches and the squad

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.

Buvio matches tab for a team, showing an upcoming tournament card and a create buttonBuvio new match form with name, match type, a date strip and the roster included by defaultBuvio welcome screen offering to create a team or join oneBuvio create team form with name, sport and crest pickerBuvio join team screen with the invite code input
04 / 04

Account and settings

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.

Buvio sign-up form with name, nickname, email, password and avatarBuvio sign-up form showing inline validation errors under the fieldsBuvio sign-in screenBuvio settings with profile, nickname, avatar and sign-outBuvio team settings with the invite code, crest and leave team
Go back to projects