← Back to portfolio
Download on the App Store View App Store listing ↗

The App

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.

Crowd Tracker SG main dashboard
Dashboard
Facility detail with weather and lightning closure overlay
Weather & closures
Weekly crowd heatmap by day and hour
Weekly heatmap
Same-day historical comparison
Same-day comparison
Custom comparison between two facilities
Facility comparison
Alert settings per favourite facility
Alert settings

The Problem

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.

Architecture — Dual-Environment Design

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:

Production — sg3

OVHCloud VPS · Singapore

  • Customer-facing FastAPI (REST)
  • PostgreSQL — historical crowd storage
  • Redis — rate limiting and session state
  • Caddy — reverse proxy and TLS termination
  • Prometheus + Grafana monitoring
Data Collection — Pi

Raspberry Pi 4 · Residential IP

  • Playwright poll agent — headless Chromium
  • sshuttle tunnel to sg3 for API writes
  • NEA weather API integration
  • 5-minute polling during operating hours

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.

ActiveSG (Playwright)
Pi Poll Agent
→ sshuttle →
FastAPI / sg3
PostgreSQL
iOS App
APNs

Data Pipeline

Real-Time Alerting System

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.

Security & Data Integrity

Authentication

  • Device-secret auth — a server-issued secret paired with a client-generated installation ID, replacing an earlier scheme vulnerable to spoofing
  • JWT-based tier verification for premium features, with real Apple App Store Server API validation of purchase receipts (sandbox and production)
  • CORS lockdown and scoped token expiry — hardened from an initial open configuration before public launch

Rate Limiting & Privacy

  • Redis-backed rate limiting per-IP and tier-aware, with a documented allowlist for known infrastructure — added after a real incident where shared NAT collapsed monitoring and dev traffic into one rate-limit bucket
  • No accounts, no PII — favourites and alert preferences are tied to an anonymous device identifier only. Deliberate privacy-by-design, not a deferred concern.

Monitoring & Observability

Notable Engineering Problems Solved

Three representative examples of debugging depth that go beyond typical CRUD work:

Silent purchase-verification failure

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.

Device-identity desync bug

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.

Production-breaking configuration gap

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.

Business Model

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.

Free
S$0
  • Live crowd levels for all facilities
  • Today's crowd chart
  • Weather and lightning-closure alerts
  • Pool reopening notifications
  • Next 3 best-hour predictions
Not affiliated with Sport Singapore or ActiveSG — this app uses their publicly available crowd data. Actively maintained post-launch, with a documented backlog of enhancements (device-token deduplication, expanded caching layer, self-service data deletion) prioritised based on real usage rather than built speculatively ahead of launch.