Skip to content

wsoule/mapr

Repository files navigation

Travel Timeline

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.


Features

  • 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)

Setup

1. Install dependencies

# Mobile
npm install

# Backend
cd backend && npm install

2. Environment variables

Root .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

3. Start PostgreSQL (Docker)

docker compose up -d

This starts a PostgreSQL instance on port 5432. Data persists in a named Docker volume.

4. Run database migrations

cd backend && npx drizzle-kit migrate

5. Start the backend

cd backend && npm run dev

6. Run the app

# iOS (native build required — Expo Go won't work)
npx expo run:ios

# Android
npx expo run:android

Note: react-native-purchases (RevenueCat) and @rnmapbox/maps require a native build. Re-run npx expo run:ios after installing new native packages.


Project Structure

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

Tech Stack

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

Architecture Notes

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.


RevenueCat Setup

  1. Create a project at app.revenuecat.com
  2. Add iOS and Android apps; copy the API keys
  3. Replace appl_PLACEHOLDER_YOUR_IOS_KEY and goog_PLACEHOLDER_YOUR_ANDROID_KEY in src/services/revenuecat.ts
  4. Create an Entitlement named exactly social
  5. Create a subscription product (monthly/annual) and attach it to the entitlement
  6. The Paywall component automatically loads packages from your default offering
  7. Copy the Secret key and set REVENUECAT_SECRET_KEY in Railway — the backend validates entitlements server-side

Backend (Railway)

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages