// PROJECT_
Echoes&Paths
01. LIVE_DEMO
// LIVE PREVIEW
Echoes&Paths →
02. PROJECT_OVERVIEW
Born from a personal need: living in Austria with a love for history, I kept opening Google Maps just to find castles, ruins or museums nearby — buried among restaurants, gas stations and everything else. I wanted one app where historical landmarks were front and center, nothing else. Echoes&Paths is that app. Cross-platform mobile (React Native + Expo SDK 54) with GPS-powered exploration, a medieval conquest system, a social layer with friends and push notifications, and full EN/DE localization auto-detected from the device. The backend aggregates Google Places API (New), Wikipedia descriptions and community contributions into a hybrid feed — server-side cached and paginated. Users can conquer places physically (GPS validated within 150m via Haversine formula on the server), build a medieval rank (Peasant → High King), set a unique username, add friends and see each other's conquered places. Photos are uploaded via Cloudinary, legal pages are GDPR-compliant and hosted on GitHub Pages. Currently in closed testing for family and friends — kept private due to Google Places API operational costs at scale. Co-developed with Claude Sonnet 4.6 (Anthropic) as a daily pairing partner throughout the entire build: architecture decisions, backend endpoints, React Native screens, i18n, push notifications and deployment.
Status
Active Development
Scope
Full-Stack Architecture
Primary Stack
React Native
Repo
Public
03. TECH_STACK & ARCHITECTURE
//Closed testing (family & friends only) — Google Places API operational costs make public scaling prohibitive at this stage
//Co-developed with Claude Sonnet 4.6 (Anthropic) — pairing partner from architecture through deployment
//Hybrid feed: Google Places (server-cached, paginated 20/page) + community historical_locations merged and deduplicated
//Conquest system: client sends GPS coords → server validates ≤150m via Haversine → upserts conquests table → returns updated medieval rank
//Social graph: friendships table drives friend requests, push notifications (Expo Push API) and cross-user conquest visibility
//expo-localization auto-detects DE/EN on startup; i18next drives all strings — categories stay as EN keys for API compatibility
//expo-notifications: push_token saved per user on login; backend fires notifications fire-and-forget on friend events
//OTA deployment: expo-updates + EAS Update pushes JS changes to production users without Play Store review
React Native 0.81 + Expo SDK 54
Frontend — Mobile Framework
Cross-platform iOS/Android from a single codebase. Expo managed workflow with expo-dev-client for native module support. EAS Build generates signed APK/AAB; expo-updates ships OTA JS patches without waiting for a Play Store review cycle.
expo-localization + i18next
Frontend — Internationalization (EN/DE)
Auto-detects device language on startup — German devices get DE, everything else defaults to EN. i18next with react-i18next drives all visible strings including category names, UI labels, GDPR footer and alerts. Categories stay as English keys internally so API filtering remains unaffected.
expo-notifications + Expo Push API
Frontend + Backend — Push Notifications
On login, the app requests permission, gets an ExpoPushToken and saves it to the users table via PUT /api/push-token. The backend calls exp.host/--/expo-push/v2/push/send fire-and-forget when a friend request is sent (🤝) or accepted (🎉). Tapping a notification navigates directly to the Friends screen and auto-selects the correct tab (Requests or Friends).
react-native-maps + expo-location
Frontend — Maps & GPS
Google Maps provider (PROVIDER_GOOGLE) with custom retro map style. expo-location fetches foreground GPS on demand — used both for the feed radius and for the Conquest proximity check. Haversine distance validation runs on the server (not the client) to prevent GPS spoofing.
Context API + AsyncStorage
Frontend — State & Persistence
AuthContext holds JWT token and user object; FavoritesContext manages saved places. Both rehydrate from AsyncStorage on cold start — session survives app restarts without re-login. Feed results cached in AsyncStorage with 24-hour TTL to reduce API calls when re-entering the same area.
Node.js ESM + Express 5
Backend — Runtime & API
Full ESM module system ('type': 'module'). Express 5 with route groups for /auth, /api (locations, contributions, conquests, friends, username, push-token). Deployed on Railway — PostgreSQL via DATABASE_URL, environment secrets in Railway vars.
PostgreSQL + Knex.js
Backend — Database
Schema managed via Knex migrations. Tables: users (username, push_token), historical_locations, conquests (UNIQUE per user+place), friendships (status: pending/accepted), location_contributions. Raw SQL via db.raw() for precise control — no ORM overhead.
Google Places API (New) + Wikipedia
Backend — Data Pipeline
Google Places searchNearby (max 20 results/call, no pageToken) is the primary feed source. Server-side in-memory cache (Map, 10-min TTL, ~1km precision key) stores all 80+ results and paginates them 20 at a time across requests. Wikipedia REST API enriches each place with historical descriptions via language-aware lookup. ?refresh=1 query param busts the cache on pull-to-refresh.
Conquest System — Haversine GPS
Backend — Feature
Users can 'conquer' a historic place by physically visiting it. The app sends both user coords and place coords to POST /api/conquests — the server calculates Haversine distance and rejects if >150m (403 too_far with exact distance). Conquest count drives a 7-tier medieval rank system (Peasant → Squire → Knight → Baron → Count → Duke → High King) returned on every conquest response.
Friends & Social Graph
Backend + Frontend — Social Layer
friendships table with UNIQUE(requester_id, addressee_id) and status enum. Endpoints: search users by @username or name, send/accept/reject/remove. Friends can view each other's conquest lists (GET /api/friends/:userId/conquests — 403 if not friends). Friend requests trigger push notifications both ways.
Community Contributions + Cloudinary
Frontend + Backend — UGC
Logged-in users can add photos or text info to any place (Google or community). Photos are uploaded client-side to Cloudinary via the Upload API before the URL is submitted to the backend — no binary data hits the Express server. Contributions are moderated (is_approved flag) before becoming publicly visible.
bcryptjs + JWT + Google OAuth
Frontend + Backend — Authentication
Email/password accounts use bcryptjs (cost 10) + JWT. Google Sign-In via @react-native-google-signin sends an ID token to the backend for verification and user upsert. JWT stored in AsyncStorage, injected as Bearer header on all protected routes via verifyToken middleware.
GDPR Legal Pages — GitHub Pages
Legal & Compliance
Full GDPR Art. 13/14-compliant Privacy Policy and Terms of Use (EU DSA) hosted as static HTML on GitHub Pages from the /docs folder of the backend repo. Data table, legal bases (Art. 6(1)(b)/(f)), retention periods, all 8 GDPR rights, DPA complaint links for 6 EU countries and ODR platform link. Contact: castleapp.tester@gmail.com.
Claude Sonnet 4.6 (Anthropic)
AI-Assisted Development
Co-developed with Claude Sonnet 4.6 as a daily engineering partner throughout the entire build — from initial architecture and backend endpoints to React Native screens, i18n strategy, push notification flow, GDPR compliance and Railway deployment. Every feature in this project was designed and iterated in direct collaboration with the model.