Side Project · iOS App · Live on App Store · July 2026
Live crowd intelligence for Singapore's ActiveSG pools and gyms — from zero public API to a launched iOS app with real-time alerting. Solo-built and operated end to end: backend architecture, data pipeline, iOS app, infrastructure, monitoring, and App Store launch.
Live crowd levels, historical trend charts, weekly heatmaps, facility comparisons, weather overlays, and per-facility push alerts — across every ActiveSG pool and gym in Singapore.
ActiveSG shows live crowd levels on its own page — but only right now, no history, no trend, no way to know whether a Tuesday morning is reliably quiet or always packed. There's no public API. Crowd Tracker SG fills the gap: real-time tracking, historical pattern analysis, weather-aware alerts, and best-time predictions across every pool and gym in Singapore.
The core insight was that the right question isn't "how busy is it now?" — it's "if I leave in 45 minutes, what will it be like when I get there, and is today tracking differently from a typical Thursday?" That requires history, context, and prediction. None of that existed.
The most interesting architectural decision was splitting the system across two environments by function, not by scale. ActiveSG has no public API — the data is scraped from their live crowd page via headless Chromium (Playwright). Cloud datacenter IPs are blocked by ActiveSG's infrastructure. The solution was deliberate:
The Pi's residential IP is undetected by ActiveSG's bot-blocking — the scraping
workload stays there, while the customer-facing API runs on production infrastructure
with a real SLA. Traffic from the Pi is tunnelled to sg3 via sshuttle,
keeping the serving layer entirely cloud-hosted while the data collection layer
operates from a residential connection that doesn't trigger blocks.
Five independently-tuned push notification types, delivered via Apple Push Notification service (APNs):
| Alert type | Behaviour |
|---|---|
| Threshold-crossing | Fires when a favourited facility's crowd drops below a user-set percentage. Asymmetric hysteresis prevents rapid re-fire when capacity oscillates at the boundary — the alert re-arms only after crowd rises to threshold + buffer. |
| Weather (bidirectional) | Rain or lightning detected and cleared. A 2-cycle debounce (2 × 5-minute polls) filters single-detection flicker without meaningfully delaying real weather changes. Validated against a live weather event during pre-launch testing. |
| Reopening | Fires the moment a facility reopens after a lightning closure — the specific piece of information regular visitors genuinely can't predict from experience alone. |
| Daily best-hour digest | Evening notification (8PM) with tomorrow's predicted quietest hour per favourited facility. |
| Weekly best-hour digest | Sunday-evening summary of the coming week's best day and hour across favourites. |
Three representative examples of debugging depth that go beyond typical CRUD work:
Root-caused a purchase verification failure to a local Xcode testing configuration transparently intercepting App Store transactions — traced through client logs, server logs, and Apple's own API error responses to distinguish "testing artifact" from "real production bug" before shipping. Would have been invisible without structured, correlated logging across the iOS client and the API.
Diagnosed authenticated requests silently failing by correlating client-side Keychain state, server-side database rows, and direct API reproduction via curl. Isolated the root cause to iOS code-signing churn during development — not a risk to real users — before deciding whether it warranted a fix rather than a workaround.
Caught during final pre-release review: an environment variable controlling Apple sandbox-vs-production purchase verification was silently defaulting to sandbox in the deployed container despite being set correctly in the environment file. Would have broken every real customer's first purchase had it shipped unnoticed. Found by reviewing the running container's resolved environment, not just the source config.
Running a backend that polls every 5 minutes across dozens of facilities, stores months of history, and fires push notifications has real infrastructure costs. I built a freemium model via StoreKit 2 rather than making the free version useless. The free tier is genuinely useful on its own — the paid tier funds the backend.