Case study · iOS app · Aug 2026
Clash of Screentime
Head-to-head screen-time challenges for friends, built with one collaborator. Lower usage wins, and the loser’s sealed selfie goes only to the winner.

- Team
- 2 developers
- App extensions
- DeviceActivity monitor and report
- Backend
- 21 Postgres tables, 16 Edge Functions
- Released
- App Store, August 2026
Overview
Clash of Screentime turns cutting phone use into a 1v1 challenge between friends. Each player seals a penalty selfie before the battle, the player with lower screen time wins, and only the winner ever sees the loser’s photo.
My role
I built this with one collaborator, and we each wrote a large share of the code. My main area was the usage pipeline: most of the screen-time service and monitor extension, the report extension, the hourly-usage Edge Function, and the event-ledger migration that scores challenges. My collaborator wrote the spike filter and the moderation and account-deletion work, and we shared the penalty-selfie flow.
How it works
- Monitor extensionFive-minute DeviceActivity threshold callbacks
- App Group storageBatches events, retries when offline
- Edge FunctionAuthenticates and records hourly events
- Event ledgerPostgres scores the challenge window
- ResultServer resolves the winner, or a tie
Engineering decisions
Scoring without being able to read usage
- Problem
- Apple’s Screen Time APIs don’t give the app raw usage numbers. A monitor extension only receives callbacks when a threshold is crossed, and it runs under tight sandbox limits.
- Approach
- The app registers repeating hourly events with five-minute thresholds, so usage arrives as a stream of callbacks. The extension writes them to App Group storage and posts them to an Edge Function, retrying if the phone is offline.
- Trade-off
- Scores are approximate five-minute bands, not exact minutes, and the app says so. That’s the most accuracy the platform allows.
Resolving results on the server
- Problem
- Neither player’s phone should get to declare the winner, and challenges can span days.
- Approach
- Events go into an append-only Postgres ledger. Writes take a row lock on the player’s registration, reject timestamps outside a server-side window, and are callable only by the service role. A range-count query over the ledger totals each player’s events inside the challenge window to resolve a winner or tie.
- Trade-off
- Timestamps still originate on the device, so this stops casual disagreements but isn’t tamper-proof. Apple doesn’t offer a server-verifiable source.
A penalty photo that can’t leak early
- Problem
- A photo of a friend is sensitive, so it must stay private unless a result is certain.
- Approach
- Each player takes and approves a selfie per challenge, which is sealed in storage. The reveal function checks for a confirmed result, gives only the winner a signed URL that expires after five minutes, and never reveals on a tie.
- Trade-off
- More states to manage and more ways for a challenge to end without a reveal. For this feature, I’d rather fail closed.
Testing
The repository includes XCTest cases, Deno tests for Edge Functions, and SQL test scripts, including one I wrote for the usage ledger. I haven’t re-run the full suite for this write-up, so I don’t quote a pass count. All 21 tables use forced row-level security.
What I’d do next
- Run the SQL and Edge Function tests in CI.
- Add reliability metrics for how often extension callbacks arrive late or not at all.