All large projects

PintRunner

An Android app for planning pub golf: a pub crawl with teams, challenges and a live scorecard. I designed and built the app, the backend and the hosting on my own.

Role

Solo developer: app, backend, hosting and website.

App

Android, written in Kotlin with Jetpack Compose.

Backend

Kotlin and Ktor with MariaDB, self-hosted behind Cloudflare.

Status

Installable Android build, being tested with friends.

Overview


Pub golf works well with a small group. With a large group, though, somebody usually ends up becoming the organiser: keeping the route, checking challenges, recording scores, answering “where are we going next?”, and trying to keep everyone moving.

PintRunner is designed to take that job away.

An organiser creates a route with the pubs, drinks and challenges, then shares the event with the group. Players join from their phones and PintRunner handles the structure of the round.

Players are split into teams, and each team is assigned a rival. The rival team records your scores and checks whether you completed your challenges, while you do the same for them. Because you don't score yourself, there is less opportunity to simply enter a better score.

During the round, the app tells players which pub they're at, what they need to drink, what challenge they're doing and where they go next. Location check-ins make sure teams actually reach the next pub before continuing.

The result is a pub golf event that needs much less organising before and during the night. The organiser sets it up; the players run the game themselves.

Events can also be made public. Instead of being limited to an existing group of friends, a public event can be discovered by people nearby and give them a way to join a local game and meet other players.

Screenshots

Swipe through a round, from finding an event to the leaderboard.

The Explore screen listing public pub golf events near the user, with a distance filter
Find public events near you
The route builder showing a map with selected pubs and a list of stops with drinks and challenges
Build a route on the map
An event page with a route map, date and time, and an invite code
Share the event with a join code
The Teams tab showing players with profile pictures split into teams
Teams, balanced when the round starts
The Journey screen showing the current pub, the drink, and rows for logging each player's score
The current pub, the drink and the scoring
The leaderboard sheet with team and individual scores
Live team and individual leaderboards
The route building map, showing selected and unselected pubs
Select your favourite local pubs

A round, step by step

  1. Find or build a route

    Search for pubs with Google Places, drop them on the map and give each stop a drink, an optional challenge and a water hazard if you want one. Routes can be private or public.

  2. Create the event

    Pick a date and start time, then share the join code or make the event public so people nearby can find it.

  3. Get everyone in

    Players join and can form their own teams. When the host starts the round, the server balances the teams and pairs each one with a rival.

  4. Play the route

    The app shows where your team is, what the drink is and what the challenge is. Your rivals log your scores, and you log theirs.

  5. Move on together

    The next pub only unlocks when your team is really there, using GPS check-ins from everyone's phone.

  6. See who won

    Team and individual leaderboards update as scores come in. If someone is spoiling the night, the group can start a removal poll.

Problems I had to solve

Scores nobody can shortcut

Teams are paired, and rivals record each other's scores, so you can't mark your own. The server, not the app, works out which pub a team is on, so tapping through entries can't skip anyone ahead.

Knowing when a team has arrived

Phones send a location ping every 20 seconds while a round is open. The server finds the nearest pub to each one with the haversine formula. A team moves on when at least half its players are within 150 m of the next pub.

Splitting people fairly

Team formation has rules of its own: at least two teams, pre-made teams kept together when they're roughly even, no lonely one-person teams, and leftovers folded into existing teams. It covers awkward cases like odd numbers and one huge pre-made team.

Release builds that don't crash

My first minified release build crashed on the map screen. R8 had stripped a class the Google Places SDK loads at runtime. I traced the obfuscated stack trace back through the mapping file, added a keep rule, and now test the release build on a real phone before sharing it.

Hosting it without a hosting bill

The API and database run on a Linux machine of my own, reached through a Cloudflare Tunnel with no port forwarding. Services start on boot through systemd, and the database is backed up every night. I've tested restoring a backup.

Looking after users

Sessions use Google sign-in and JWTs. Profile photos are resized and re-encoded on the phone, which also strips location metadata. There's in-app account deletion, plus a privacy policy and terms on the website.

How it's built

  • Kotlin
  • Jetpack Compose
  • Material 3
  • Ktor
  • kotlinx.serialization
  • JWT
  • Google sign-in
  • Google Maps
  • Google Places
  • Coil
  • MariaDB
  • Supabase Storage
  • Cloudflare Tunnel
  • systemd
  • R8

The app talks to a Ktor API over HTTPS. Cloudflare handles the certificate and forwards traffic through a tunnel to the server, which runs Ktor and MariaDB. Profile photos are stored in Supabase, and the map, place search and sign-in come from Google.

Diagram: the Android app calls Cloudflare, which forwards through a tunnel to a Ktor API backed by MariaDB and nightly backups. The app also uses Google services, and the API stores profile photos in Supabase.

What's next

An iPhone version, which can reuse the same backend, and a Google Play release once the pilot rounds are done.

Want to talk about how it's built?

I'm happy to walk through the code, the architecture or the decisions behind it.