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.

The Clash of Screentime profile screen on iPhone, with account and safety settings.
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

From phone usage to a challenge result
  1. Monitor extensionFive-minute DeviceActivity threshold callbacks
  2. App Group storageBatches events, retries when offline
  3. Edge FunctionAuthenticates and records hourly events
  4. Event ledgerPostgres scores the challenge window
  5. 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.