An interactive vacation photo album with social features. Pin trips on a Mapbox map, upload photos, browse your history in a vertical timeline, and share publicly with friends.
Free: Fully offline — map, photos, timeline, itinerary. No account needed. Premium: Cloud sync, public sharing, likes, comments, friends, and social feed.
- Map — Interactive Mapbox map; long press to drop a pin; zoom-level controls (World → Street)
- Pin trips — Auto reverse-geocodes to city/region/country
- Trip details — Title, date range, photos, lightbox viewer with captions, itinerary
- Timeline — Chronological list; search and sort; stats (trips, countries, cities)
- Offline map tiles — Download current bounds for offline use
- Light & dark mode — Follows system setting
- Social feed — Friends' public trips (Premium)
- Likes & comments — On any public trip (Premium)
- Friends — Send/accept/reject friend requests, search users (Premium)
- Cloud sync — Trips replicated to backend automatically (Premium)
- Visibility control — Private / Friends / Public per trip (Premium)
# Mobile
npm install
# Backend
cd backend && npm installRoot .env (mobile):
EXPO_PUBLIC_MAPBOX_TOKEN=pk.your_public_token_here
RNMAPBOX_MAPS_DOWNLOAD_TOKEN=sk.your_secret_token_here
EXPO_PUBLIC_API_URL=http://localhost:3000
EXPO_PUBLIC_APPLE_CLIENT_ID=com.yourapp.bundle
EXPO_PUBLIC_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
backend/.env (copy from backend/.env.example):
DATABASE_URL=postgres://timeline:timeline@localhost:5432/travel_timeline
JWT_SECRET=a_long_random_string
JWT_REFRESH_SECRET=another_long_random_string
APPLE_CLIENT_ID=com.yourapp.bundle
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
PORT=3000
UPLOADS_DIR=./uploads
BASE_URL=http://localhost:3000
Add the Mapbox secret token to ~/.netrc for CocoaPods (iOS builds):
machine api.mapbox.com
login mapbox
password sk.your_secret_token_here
docker compose up -dThis starts a PostgreSQL instance on port 5432. Data persists in a named Docker volume.
cd backend && npx drizzle-kit migratecd backend && npm run dev# iOS (native build required — Expo Go won't work)
npx expo run:ios
# Android
npx expo run:androidNote:
react-native-purchases(RevenueCat) and@rnmapbox/mapsrequire a native build. Re-runnpx expo run:iosafter installing new native packages.
app/
(auth)/
login.tsx # Sign in — email, Apple, Google
register.tsx # Create account
(tabs)/
index.tsx # Map screen
timeline.tsx # Timeline screen
feed.tsx # Social feed (Premium)
profile.tsx # Profile, friends, settings
trip/
new.tsx # Add trip modal
[id].tsx # Trip detail (likes, comments, visibility)
edit/[id].tsx # Edit trip
backend/
src/
db/
schema.ts # Drizzle schema (users, trips, photos, social tables)
index.ts # PostgreSQL connection
routes/
auth.ts # Register, login, Apple, Google, refresh, logout
trips.ts # Trip CRUD
photos.ts # Photo upload
social.ts # Likes, comments, feed, friends
users.ts # Profile, user search
middleware/
authenticate.ts # JWT verification
src/
types/index.ts # Core types (Trip, Photo, User, Comment, Friendship…)
store/
tripsStore.ts # Zustand — trip CRUD + photo persistence + background sync
authStore.ts # Auth state + premium status
socialStore.ts # Feed, comments, friends, likes
services/
storage.ts # AsyncStorage (offline source of truth)
syncService.ts # Background replication to backend (premium users)
api.ts # Fetch wrapper (auth headers, auto-refresh)
authService.ts # Login/register/OAuth + expo-secure-store
revenuecat.ts # RevenueCat SDK wrapper
geocoding.ts # Mapbox reverse geocoding
theme/
colors.ts # Light/dark color tokens + Mapbox style URLs
index.ts # useTheme() hook
components/
premium/
PremiumGate.tsx # Gate wrapper: not-logged-in / not-premium / premium
Paywall.tsx # Subscription modal
social/
LikeButton.tsx # Heart toggle with count
CommentsModal.tsx # Comments bottom sheet
FriendRow.tsx # Friend list item (accept/reject/remove)
map/
TripMarker.tsx
OfflinePacks.tsx
timeline/
TripCard.tsx
trip/
PhotoPicker.tsx
DateRangePicker.tsx
ItinerarySection.tsx
| Package | Version | Purpose |
|---|---|---|
| Expo | 55 | Build toolchain |
| Expo Router | 55 | File-based navigation |
| @rnmapbox/maps | 10 | Map, clustering, offline tiles |
| Zustand | 5 | State management |
| AsyncStorage | 2 | Offline-first local storage |
| expo-secure-store | 55 | Secure JWT storage |
| expo-auth-session | 55 | Apple + Google OAuth |
| react-native-purchases | 8 | RevenueCat premium |
| Fastify | 5 | Backend API |
| Drizzle ORM | 0.44 | Type-safe PostgreSQL queries |
| jose | 5 | JWT + Apple/Google token verification |
Offline-first: AsyncStorage is always the source of truth. The app works completely offline without an account. SyncService replicates to the backend in the background when a user is authenticated and has Premium — writes are fire-and-forget so network failures never affect the local experience.
Premium enforcement: RevenueCat manages subscriptions. The backend verifies entitlements server-side by calling the RevenueCat REST API (/v1/subscribers/{userId}) using a secret key — responses are cached for 30 seconds. The mobile user ID is the RevenueCat subscriber ID (set via Purchases.logIn(userId) on every auth event). Set REVENUECAT_SECRET_KEY in Railway to enable this. See backend/src/services/revenuecat.ts.
Auth is optional: Nothing in the core trip-logging flow requires a user account. app/_layout.tsx loads saved tokens on startup but never redirects to a login screen.
Adding a new premium feature: Wrap the component in <PremiumGate featureName="…"> on the mobile side, and call await isPremiumUser(userId) in the Fastify route handler.
- Create a project at app.revenuecat.com
- Add iOS and Android apps; copy the API keys
- Replace
appl_PLACEHOLDER_YOUR_IOS_KEYandgoog_PLACEHOLDER_YOUR_ANDROID_KEYinsrc/services/revenuecat.ts - Create an Entitlement named exactly
social - Create a subscription product (monthly/annual) and attach it to the entitlement
- The
Paywallcomponent automatically loads packages from your default offering - Copy the Secret key and set
REVENUECAT_SECRET_KEYin Railway — the backend validates entitlements server-side
The API is hosted at https://mapr-production.up.railway.app
Required Railway environment variables (set in the mapr service Variables tab):
DATABASE_URL auto-injected by the Postgres plugin
JWT_SECRET openssl rand -base64 48
JWT_REFRESH_SECRET openssl rand -base64 48
REVENUECAT_SECRET_KEY sk_... (RevenueCat project → API Keys)
APPLE_CLIENT_ID com.traveltimeline.app.web
GOOGLE_CLIENT_ID your-web-oauth-client-id.apps.googleusercontent.com
PORT 3000
BASE_URL https://mapr-production.up.railway.app
UPLOADS_DIR ./uploads
Redeploy after setting vars:
cd backend && railway up --detach