Back to Naptown Labs

Case Study

Powerhouse Fitness

A full-featured mobile fitness app built with React Native, Firebase, and RevenueCat — offline-first, AI-powered, and subscription-ready.

906 Exercises
25 Screens
5 Programs
4 Themes
0 Custom Backend

Stack

No custom server. Firebase + managed services handle everything from auth to sync to billing.

Frontend

React Native with Expo SDK 54, running React 19 on the new architecture. Hand-built UI with no component library — every screen uses RN primitives, react-native-svg, and a custom theme system.

React Native 0.81 Expo 54 React 19 SVG

Backend

Firebase Auth for identity, Firebase Realtime Database for cloud sync, and a thin VPS proxy at api.harmjoy.us for AI routine generation (GPT-4o-mini) and push token management.

Firebase Auth Firebase RTDB VPS Proxy OpenAI

Integrations

RevenueCat for subscription billing across platforms. Health Connect for step, weight, and workout sync. Google Drive for encrypted cloud backups. Expo Notifications for push.

RevenueCat Health Connect Google Drive Expo Push

Architecture

A fat client with managed services. The app owns all state; Firebase provides sync and auth; RevenueCat handles billing.

// Provider tree (App.js) SafeAreaProvider ErrorBoundary AuthProvider // Firebase auth state ThemeProvider // 4 themes, 8 accents, scaling SubscriptionProvider // RevenueCat Pro/trial ToastProvider // Global toast system SocialProvider // Friends, notifications AppGate // Auth state machine WorkoutApp // The actual app (~2,440 lines) // Navigation: custom state-based, NOT React Navigation WorkoutApp owns currentScreen state + ~40 useState calls | |-- setCurrentScreen('home') // just a setState |-- Props drilled to every screen // no Redux, no Zustand |-- BottomTabBar (5 tabs) // home, train, browse, stats, me '-- MenuDrawer (14 links) // hamburger slide-in // Data sync: offline-first, last-write-wins AsyncStorage (local blob) <---> Firebase RTDB (cloud) | | | save every mutation | pull on login/foreground | debounced push (3s) | merge by lastSaved timestamp '-- single JSON blob '-- validated before trust

Data Flow

What happens when a user completes a workout — a cascade of 7 side effects from a single action.

  1. 1 Local state update — WorkoutApp updates workoutHistory, personal records, and checks achievement predicates
  2. 2 AsyncStorage persist — Single JSON blob written to local storage immediately
  3. 3 Cloud sync — Debounced push (3s) writes to Firebase RTDB at users/{uid}/appData
  4. 4 Health Connect — Writes an ExerciseSession (type 58: strength training) with duration and calories
  5. 5 Widget refresh — Android home screen widget updates streak count and weekly total
  6. 6 Notifications — Recalculates streak-at-risk alerts and goal deadline reminders
  7. 7 Activity feed — Posts to RTDB activityFeed so friends can see and cheer the workout

Authentication

Three sign-in paths that converge into one Firebase UID. Anonymous accounts upgrade seamlessly without data loss.

Sign-in Methods

Guest — signInAnonymously(), full app access, upgradeable
Email — Firebase email/password auth
Google — PKCE S256 flow via expo-auth-session

On Auth Success

RevenueCat logIn(uid) → pullFromCloud(uid) → registerPushToken → setUserPublicProfile → registerEmailLookup (SHA-256 hash for friend discovery)

Monetization

Freemium with RevenueCat. A 7-day trial with write-once RTDB timestamps prevents trial resets.

Free

3 custom routines
7-day workout history
All 906 exercises
Health Connect sync

Pro

Unlimited routines
Full history
AI routine generation
Programs & progress photos
Google Drive backups

$0.99/mo $4.99/yr $14.99/life

Anti-Abuse

Trial start is a write-once RTDB timestamp. Firebase rule: !data.exists(). Can't be reset by reinstalling.

Security Rules

Firebase RTDB rules act as the authorization layer. No custom server needed for access control.

Path Rule Why
appData Owner-only read/write User's workout data is private
publicProfile Auth read, owner write, field validation Regex on fitnessLevel, length limits on bio
notifications fromUid === auth.uid Anti-spoofing: can't send notifications as someone else
trialStart Write-once (!data.exists()) Prevents trial timestamp manipulation
sharedGoals Friends-gated via RTDB rule ref Only friends can read shared goals
emailLookup No anonymous, value === auth.uid SHA-256 hashed emails for friend discovery

Key Features

Everything from AI coaching to encrypted backups, built with no external UI libraries.

Coach Marcus

AI avatar trained via Flux.1 Dev + LoRA on Replicate. 12 curated images, 80+ messages. Deterministic daily rotation seeded by dayHash(YYYYMMDD) — same content all day, rotates at midnight. 4 touchpoints: home card, AI modal, rest timer tips, workout summary.

AI Routines

Premium users can generate personalized routines. Sends equipment, goals, fitness level, muscle focus, and recent activity to GPT-4o-mini via the VPS proxy. Returns a structured routine ready to start.

Encrypted Backups

Google Drive backups using the hidden appDataFolder. AES-256 encryption with a passphrase derived from the Firebase UID. Up to 5 versions kept. Legacy unencrypted backups handled transparently on restore.

Health Connect

Android-only integration for steps (polled every 5min), weight (bidirectional sync), and workout sessions. Writes ExerciseSession records post-completion with duration and calorie data.

Smart Notifications

Three Android channels (workout reminders, streak alerts, goal deadlines). Weekly reminders scheduled per-day from the planner. Streak-at-risk fires at 7 PM if no workout logged today.

Home Screen Widget

Android widget built with react-native-android-widget. Shows today's routine, streak, weekly count, and top goal progress. Updates every 30 minutes. Tap to open the app.

Theme System

4 base themes, 8 accent colors, 4 shape presets, 4 font sizes, and a compact mode. All resolved at runtime and applied via useMemo + StyleSheet.create.

Themes

Dark Light AMOLED Midnight

Shape presets: sharp (0x), minimal (0.35x), rounded (1x), pill (1.6x) — applied via r() radius scaler. Font sizes: small (0.88x) through xlarge (1.24x) via fs() scaler.

How It Works

resolveTheme(themeId, accentId) merges a base theme with an accent color at runtime. ThemeContext provides colors, r(), fs(), and compactMode to every component. No styled-components, no Tamagui.