Architecture Overview
Haunts.io is a three-tier platform: a PHP REST API serving two client applications (React 19 web and React Native mobile), backed by a dual-database MySQL architecture with Firebase Authentication as the identity layer.
System Topology
The platform separates concerns across three distinct layers. The API layer (67 PHP endpoints) handles all business logic, authentication, rate limiting, and data access. The web client (React 19 + TypeScript + Vite) provides the full desktop and mobile browser experience with 28 pages. The mobile client (React Native + Expo SDK 54) delivers a native iOS/Android experience with 23 screens, push notifications, and home screen widgets.
All three layers share the same Firebase Authentication identity. The API validates Firebase ID tokens on every request, maintaining a stateless, horizontally scalable architecture even though the current deployment runs on a single Raspberry Pi.
Dual-Database Design
The most distinctive architectural decision is the dual-database design. swarmdata is a read-only MySQL database containing the full Foursquare Swarm export: check-ins, venues, photos, and metadata imported from a data export ZIP file. This database is never written to by the application; it serves as the canonical source of location history.
haunts is the read-write database containing all application state: user annotations (notes, tags, ratings, reviews), social data (follows, comments, likes), gamification state (stickers, achievements, mayors, streaks), collections (lists, trips, bucket list), and user preferences. This separation ensures that the imported historical data remains pristine while the application enriches it with new context.
API endpoints frequently JOIN across both databases in a
single query, combining Swarm history with Haunts
annotations to present a unified view. For example, the
check-in feed query joins
swarmdata.checkins with
haunts.favorites,
haunts.sticker_log, and
haunts.checkin_photos to render each card with
its full context.
API Design
Endpoint Organization
The 67 API endpoints are organized into functional domains, totaling 7,824 lines of 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 swarmdata database 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 to normalized tables.
The haunts database contains 20+ tables managed through 33+ 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 SQL migration
files (001_init_haunts.sql through
033+). Each migration is idempotent where
possible, using CREATE TABLE IF NOT EXISTS and
INFORMATION_SCHEMA-based conditional
ALTER TABLE statements (since MySQL does not
support ADD COLUMN IF NOT EXISTS). Migrations
are applied via SSH to the production server and executed
through the MariaDB CLI.
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 Raspberry Pi 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
Raspberry Pi behind a
Cloudflare Tunnel, eliminating the need for
port forwarding or a static IP. Apache with MPM event and
PHP-FPM (PHP 8.2) handles HTTP requests. OPcache with
validate_timestamps=0 ensures bytecode is
cached aggressively; PHP-FPM is reloaded after each
deployment to pick up changes.
Database Layer
MySQL 9.6 runs in a Docker container on a Synology DS923+ NAS, separating database I/O from application processing. The NAS provides redundant storage with automatic snapshots. Connection pooling and query optimization (indexed JOINs, selective column fetches, pagination limits) keep response times under 200ms for most endpoints despite the modest hardware.
CDN & Caching
Cloudflare handles DNS, SSL termination,
DDoS protection, and static asset caching with 1-year
Cache-Control headers and
?v=N query string cache-busting.
Fastly 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 via
mod_deflate.
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 cron 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.