Technical Deep-Dive

Haunts.io:
Architecture of a Full-Stack Check-In Platform

How I built a complete Foursquare Swarm replacement in five days: 67 PHP API endpoints, a React 19 web app, a React Native mobile app, a gamification engine, 20 background jobs, and dual-database architecture, all on a Raspberry Pi.

By Michael Pierce · MDPSync · 2026

67 API Endpoints Dual-Database React + React Native 20 Cron Jobs 5-Day Sprint
Section I

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.

Section II

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.

Section III

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.

Section IV

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.

Section V

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.

Section VI

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.

Section VII

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
Section VIII

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.

Section IX

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.


Or call us directly: 703.996.3037