Architecture Overview
Haunts.io runs across three surfaces and one backend. A React 19 web app, a React Native mobile app, and a Kotlin Compose watch companion all talk to the same REST API, backed by a database with two halves and Firebase Authentication as the identity layer.
System Topology
The web app is a React 19 single-page bundle with TanStack Query for data, MapLibre and D3 for the maps and the great-circle flight arcs, and Zustand for the small handful of client stores that need to outlive a route change. The mobile app is React Native on Expo SDK 54 with native modules for the camera, the location stack, push, and the home-screen widgets. The watch app is a Kotlin Compose companion that ships inside the same Android binary and signs in through the phone instead of holding its own login.
All three surfaces talk to one REST API that handles every read and write, every background job dispatch, and every external integration. The API validates a Firebase ID token on every request, applies per-user and per-IP rate limits, then calls into the handler. No request reaches the database without crossing that bootstrap chain.
Database With Two Halves
The database is split into two halves that serve different jobs. One half is the imported history: the full Foursquare Swarm export of check-ins, venues, photos, and metadata, loaded once from a data export ZIP and never written to again. It is the canonical record of where the user has been, frozen in time.
The other half is the live data for everything created since launch: user annotations (notes, tags, ratings, reviews), social data (follows, comments, likes), gamification state (stickers, achievements, mayors, streaks, points and XP), collections (lists, trips, bucket list), flights and trip share tokens, and user preferences. The split keeps the imported archive pristine while the running product enriches it with new context.
API endpoints frequently JOIN across both halves in a single query, combining the imported check-in history with the live annotations to present one unified view. The check-in feed query, for example, joins the imported check-in row with favorites, sticker log entries, and uploaded photos so each card renders with its full context in one round trip.
API Design
Endpoint Organization
The 77 API endpoints are organized into functional domains, totaling 8,773 lines of endpoint PHP:
| Domain | Endpoints | Purpose |
|---|---|---|
| Core | 12 | Check-ins, venues, photos, search, nearby |
| Social | 10 | Feed, follow, profile, comments, likes, block |
| Gamification | 8 | Achievements, stickers, leaderboard, streaks |
| Collections | 12 | Lists, trips, bucket, countries, revisit, tags |
| Enrichment | 8 | Venue details, ratings, reviews, notes, weather |
| User | 7 | Settings, export, import, verification, telemetry |
| Admin | 6 | Dashboard, analytics, users, reports, system, audit |
| Integrations | 4 | Spotify OAuth, push registration, preferences |
Authentication & Security
Every API request requires a valid Firebase ID token in the
Authorization: Bearer header. The
firebase-auth.php middleware verifies the token
using the Firebase Admin PHP SDK (kreait/firebase-php),
extracts the user ID, and enforces a single-user allowlist
for the initial deployment phase.
The api-security.php layer adds rate limiting
(per-endpoint configurable), CORS enforcement with explicit
origin whitelisting, content-type validation, and request
size limits. All database queries use parameterized
statements to prevent SQL injection.
Response Patterns
All endpoints return a consistent JSON envelope:
{"success": true, "data": ...} on success, or
{"success": false, "error": "message"} on
failure with appropriate HTTP status codes. Paginated
endpoints include total, page, and
limit metadata. The API supports both
GET query parameters and POST JSON
bodies depending on the operation semantics.
Database Architecture
Schema Design
The imported history half mirrors the
Foursquare data export structure: checkins,
venues, categories, and
photos tables with foreign key relationships.
This schema is populated by an import worker that parses the
Foursquare ZIP export and maps it into normalized tables.
The live data half holds 20+ tables managed through 45+ sequential migrations:
| Category | Tables | Purpose |
|---|---|---|
| User Data | users, user_settings, blocked_users | Profiles, preferences, privacy, home location |
| Annotations | favorites, notes, tags, ratings, reviews | Per-venue user content overlaid on Swarm data |
| Gamification | stickers, sticker_log, achievements, achievement_log, mayors, streaks | 49 sticker definitions, 32 achievements, unlock tracking |
| Social | follows, comments, likes, feed_events | Friend graph, engagement, activity broadcasts |
| Collections | lists, list_items, trips, trip_days | Custom venue lists, auto-detected trip grouping |
| Enrichment | venue_cache, weather_cache, checkin_photos | Foursquare Places data, historical weather, photo uploads |
| Push | push_tokens, push_prefs | Device registration, per-category notification preferences |
Migration Strategy
All schema changes are tracked as numbered, idempotent SQL
migration files. Each migration uses
CREATE TABLE IF NOT EXISTS and
INFORMATION_SCHEMA-based conditional
ALTER TABLE statements where possible, since
the underlying engine does not support
ADD COLUMN IF NOT EXISTS. Migrations are
applied through a remote shell command against the
production database.
Gamification System
The gamification engine is the heart of the Swarm replacement experience: 49 stickers, 32 achievements, a mayor system, streaks, leaderboards, and post-check-in engagement nudges.
Sticker System
49 custom stickers are organized into five rarity tiers: Common, Uncommon, Rare, Epic, and Legendary. Each sticker has a set of contextual rules that determine when it is awarded. Rules evaluate venue category (coffee shop, bar, airport, gym), time of day (morning, late night), visit frequency (first visit, 10th visit, 50th visit), and special conditions (checked in during rain, visited 5 countries).
The sticker rule engine runs as both a real-time
post-check-in evaluation and a batch cron job
(jobs/contextual_stickers.php) that
retroactively evaluates all historical check-ins against
newly added rules. This ensures that importing 6,700+ Swarm
check-ins immediately populates the sticker collection
without requiring manual check-ins.
Achievement System
32 achievements with XP values track cumulative progress:
venue count milestones, city/country exploration targets,
streak lengths, photo counts, and category-specific goals
(visit 20 coffee shops, check in at 10 airports). The
achievement evaluator
(jobs/evaluate_achievements.php) runs on a
schedule and compares current stats against threshold
definitions, awarding new achievements and broadcasting
unlock events to the social feed.
Mayor System
The mayor system tracks the user with the most check-ins at each venue. Mayor status is recalculated on each check-in and displayed on venue pages with competitive context ("You need 3 more visits to become mayor"). Mayor crowns appear on profile stats and contribute to the overall gamification score.
Engagement Nudges
After each check-in, the API evaluates near-miss achievements and returns contextual nudges: "You’re 2 check-ins away from Explorer Level 3!" or "First time in this city!" These nudges drive continued engagement without requiring the user to navigate to the achievements page.
Mobile App Architecture
Technology Choices
The mobile app uses React Native 0.81 with Expo SDK 54, targeting both iOS and Android from a single TypeScript codebase. Navigation uses React Navigation v7 with a bottom tab navigator (Feed, Map, Stats, Profile) and a native stack for modal screens. State management combines React Context for authentication and TanStack React Query for server state caching.
Key Screens (23 total)
| Category | Screens | Features |
|---|---|---|
| Core Tabs | Feed, Map, Stats, Profile | Infinite scroll, MapLibre clustering, bento grid, collection launchpad |
| Check-In Flow | Nearby, Compose, Confirm | GPS venue search, photo attach, note entry, sticker preview |
| Collections | Stickers, Achievements, Lists, Trips, Bucket, Countries | Progress tracking, rarity filters, trip route maps |
| Social | Public Profile, Followers, Following | Friend management, activity viewing |
| Settings | Profile Editor, Notifications, Home Location, Spotify, Blocked, Export | MapLibre location picker, OAuth, push preferences |
Native Integrations
Maps: MapLibre GL Native provides the mapping layer with venue pin clustering, heatmap overlay, and a home location picker in settings.
Push Notifications: Expo Notifications with
Firebase token registration. Deep linking via the
haunts:// URL scheme routes notification taps
to the relevant screen (venue, check-in, profile).
Home Screen Widgets: iOS WidgetKit (SwiftUI) and Android AppWidget provide a quick check-in button that deep-links to the Nearby screen. The widget is static (no live data), serving as a launch shortcut.
Offline Resilience: A pending check-in queue stores locally created check-ins and flushes them to the API every 15 seconds when the app returns to the foreground. React Query provides stale-while-revalidate caching for all server state.
Background Job System
20 PHP cron jobs handle enrichment, evaluation, and notification tasks that would be too expensive or slow to run inline with API requests.
| Category | Jobs | Purpose |
|---|---|---|
| Gamification | evaluate_achievements, contextual_stickers, seed_achievements, seed_stickers | Batch evaluate unlock conditions, seed initial data, retroactive sticker assignment |
| Enrichment | weather_backfill, venue_enrichment, photo_migration | Historical weather for all check-ins, Foursquare Places data, CDN photo migration |
| Detection | trip_detection, streak_calculator | Auto-group check-ins 100+ km from home into trips, calculate active streaks |
| Integrations | spotify_poll, import_export_worker | Poll Spotify playback context, process Swarm data imports/exports |
| Push Notifications | push_near_achievement, push_on_this_day, push_streak_save, push_weekly_summary | Engagement nudges, "on this day" memories, streak reminders, weekly recap |
| Maintenance | photo_cleanup, cache_refresh | Orphaned photo cleanup, venue cache invalidation |
Jobs are scheduled via system cron on the web server. Each job is a standalone PHP script that connects to both databases, processes a batch of records, and logs its results. Jobs are designed to be idempotent and resumable: if interrupted, they pick up where they left off on the next run.
Third-Party Integrations
| Service | Purpose | Integration Method |
|---|---|---|
| Firebase Auth | Identity and authentication (Google + Apple Sign-In) | kreait/firebase-php SDK, ID token verification on every API request |
| Foursquare Places API | Venue search, details, ratings, hours, photos | REST API with server-side caching in venue_cache table |
| Google Places API | Nearby venue lookup for mobile check-in | REST API with radius-based search from device GPS |
| Spotify API | Capture currently playing song at check-in time | OAuth 2.0 flow with refresh token, background polling cron |
| Weather API | Historical weather for every check-in | Batch backfill cron job with per-checkin lat/lng lookup |
| Mailgun | Email verification for new accounts | Transactional email via SMTP |
| Expo Push | Cross-platform push notifications | Device token registration, server-side push via Expo API |
Performance & Infrastructure
Server Architecture
The production stack runs on a dedicated application server behind an edge layer that handles DNS, TLS termination, and DDoS protection. A persistent application server with bytecode caching keeps response times tight, and the cache is reloaded after each deployment so new code takes effect immediately.
Database Layer
The database runs on a dedicated host, separating storage I/O from application processing. Redundant storage with automatic snapshots protects against data loss. Connection pooling and query optimization (indexed JOINs, selective column fetches, pagination limits) keep response times under 200ms for most endpoints.
CDN & Caching
The edge layer handles DNS, TLS termination, DDoS
protection, and static asset caching with 1-year
Cache-Control headers and a
?v=N query string for cache-busting. A separate
image CDN serves user-uploaded photos with on-the-fly
resizing (original, cap300, cap600, 500x500 variants).
Static web assets (JS, CSS, fonts) are served with gzip
compression.
Performance Optimization
A dedicated performance pass on Day 3 addressed 21 identified bottlenecks: batched database queries replacing N+1 patterns, React.memo and useMemo for expensive component renders, lazy loading for below-the-fold images, query result memoization in the API layer, and connection reuse across background job batches.
Day-by-Day Build Timeline
| Day | Date | Focus | Key Deliverables |
|---|---|---|---|
| 1 | April 10 | Full Stack Launch | Backend API (Phases D-J), React web app, history feed, venue pages, heatmap, stats, gamification, lists, trips, Spotify, weather backfill, SEO, legal pages |
| 2 | April 11 | Mobile + Polish | React Native app (Phases L-N): check-in flow, MapScreen, stats, heatmap overlay, push notifications, landing page redesign, 3-year review |
| 3 | April 12 | Social + Performance | Comments, coincidence engine, weekly leaderboard, 49-sticker overhaul (5 rarity tiers), enriched venue details, performance pass (21 fixes) |
| 4 | April 13 | Data + Widgets | Foursquare Places API migration, Swarm data import/export, contextual sticker auto-assignment, add-to-list flows, iOS WidgetKit + Android widgets, social profiles |
| 5 | April 14 | Enrichment + Polish | Venue notes/tags, achievement nudges, first-time celebrations, mayor badges, countries page, revisit list, leaderboard tabs, feed broadcasts, coincidence detector, trip photos |
The web app and complete backend API launched on Day 1. The mobile app shipped on Day 2. Days 3-5 added social features, refined the gamification system, migrated third-party APIs, and polished the user experience. Every phase followed the same pattern: architect makes design decisions, AI executes implementation, architect reviews and deploys.
Ready to Build Something Extraordinary?
Let MDPSync bring your vision to life with vibe-coding. From concept to production in record time.