Kay Smits
All projects

Pigeon Mail

Mobile game where pigeons carry your messages across a real world map.

Role
Game, backend and infrastructure
Period
2025 - 2026
Stack
  • Unity 6
  • C#
  • Laravel
  • Mapbox
  • Firebase

You send a message with a pigeon that actually travels a route across the map and takes time doing it. Around that: accounts, friends, a shop and stats.

The Pigeon Mail staff portal

The pigeon is server state, not app state

A journey is a database row with a starting point, a current position and a timestamp. A queued job advances every active journey by that pigeon's flying speed in metres per second, writes a log line with the distance covered, and marks the journey arrived once it reaches the destination. The app only draws what the server says. Two devices cannot disagree about where the same pigeon is, and closing the app does not stop it flying.

Coordinates encrypted at rest

Latitude and longitude go through an encrypted cast, so they are not readable in the table. For a game built on real locations that is the difference between a breach handing over a map of where people live and a column of noise. There is an endpoint for a player to request account deletion, and friend locations sit in a cache that is cleared separately.

From Niantic to Mapbox, not by choice

The project stopped opening: Niantic pulled the Lightship map packages offline and the package manager hung on a 404. Moved to Mapbox v3 with five vector layers on the existing pixel-art materials, a permanent tile cache and world scale 8. Three unrelated things came along for the ride: a render API removed in URP 17, a misplaced using, and a duplicate Newtonsoft DLL.

One monolith became five assemblies

Every C# script sat in Assembly-CSharp: each change recompiled everything and no boundary existed between layers. There is now a core without a Unity dependency that tests in a tenth of a second without loading a scene, alongside the API, game and editor layers. The compiler immediately flagged hidden dependencies and stopped a Unity logger from living in the pure layer.

A failure that looked like a hang

If anything threw inside a coroutine it died silently: no callback, no message, just an app that stopped doing things. Seen twice in production and impossible to reproduce with curl. The API layer now returns a result type that cannot be empty, and a try/catch guarantees exactly one callback. On top of that an error kind that says whether retrying makes sense and whether the session expired.

Version gating that does not lock everyone out

The app compared its version to the API version and would have locked everyone out at the next release. There is now a minimum per platform, set through an environment variable without a deploy, with real semver comparison and an escape for reviewers. An Android hotfix no longer drags iOS along.

What is in it

  • Messages and journeys A message gets a pigeon; the journey runs in real time with a log per step.
  • Friends Search by distance, send requests, accept or decline.
  • Shop and currency Pigeons with their own base stats, flying speed among them, bought with in-game transactions.
  • Rewards and stats Per-account progress, with rewards tied to milestones.
  • Push notifications Firebase Cloud Messaging with a device token per handset, so arrival lands while the app is closed.
  • Staff portal A separate web environment for the team, apart from player accounts.