# FytOS / Livewire -- Expo App Integration

Integrate an Expo React Native app with the FytOS/Livewire backend API. Use when building mobile app screens, implementing authentication, fetching content, handling deep links, or connecting to any FytOS backend endpoint. Covers universal API key flow, voucher-based registration, client-scoped content, live classes, session tracking, and AI features.

# Backend Overview

  • Base URL: https://your-api-host/api/v1 (dev: http://localhost:3010/api/v1)
  • Protocol: REST over HTTPS, JSON request/response bodies
  • Auth model: API key (header) + JWT bearer tokens (per-user)

Postman collections are the source of truth for every endpoint, request body, and response shape. They are included alongside this skill and should be referenced for exact payloads.


# Authentication Architecture

There are two API key modes. The app should support both.

# 1. Universal Key (Livewire single app)

Used when a single app serves users from multiple partners/clients. The client context is resolved dynamically.

Header Value
X-API-Key The universal key (env: UNIVERSAL_API_KEY)

How client context works:

  • App launch: GET /app/config (public, no key needed) returns default client's branding and enabled features
  • Deep link / onboarding: GET /app/join/:voucherCode (public, no key needed) returns specific client branding
  • Registration: voucher code in body maps user to a client; if no voucher, the default client is used
  • Login: user looked up globally by email; client resolved from user record
  • Authenticated routes: client resolved from JWT clientId claim; /auth/me always returns data.client.enabledFeatures

# 2. Per-Client Key (dedicated partner app)

Used when a partner has their own branded app with a dedicated API key.

Header Value
X-API-Key Client-specific key (starts with fytos_...)

All requests are automatically scoped to that client.


# App Launch / Feature Discovery

On first launch (before any auth), the app must determine which client context to use and what features to present. This drives the entire UX: registration screens, tab bar items, and available functionality.

# Flow

1. App launches
2. GET /api/v1/app/config                (public, no auth/key needed)
3. Response: { data: { hasDefaultClient, requiresVoucher, client } }
4. If hasDefaultClient is true:
   - Use client.enabledFeatures to build the onboarding/registration flow
   - Use client.logo / client.splashScreen for branding
   - Registration can proceed without a voucher (user assigned to default client)
5. If hasDefaultClient is false (requiresVoucher is true):
   - Prompt the user for a voucher code or wait for a deep link
   - Validate with GET /api/v1/app/join/{code}
6. After login/register:
   - GET /api/v1/auth/me always returns data.client.enabledFeatures
   - Use this to gate features in the authenticated app

# Example: App Config Check

interface AppConfig {
  hasDefaultClient: boolean;
  requiresVoucher: boolean;
  client: {
    id: string;
    name: string;
    logo: string | null;
    splashScreen: string | null;
    enabledFeatures: string[];
  } | null;
}

const getAppConfig = async (): Promise<AppConfig> => {
  const res = await fetch(`${API_BASE}/app/config`);
  const { data } = await res.json();
  return data;
};

// On app launch:
const config = await getAppConfig();
if (config.hasDefaultClient && config.client) {
  // Use config.client.enabledFeatures to build navigation
  // Show config.client.splashScreen if available
} else {
  // Show voucher input screen or deep link landing
}

# enabledFeatures Reference

Feature Key What It Unlocks
classes On-demand video classes, class browsing, class plans
live_classes Scheduled live streaming classes, calendar view
workout_builder AI workout generation, workout player, workout plans
nutrition Nutrition tracking, food scanning, nutrition plans
coach_connect Trainer browsing, booking one-on-one sessions
ai_coach AI coaching via voice/chat (Hume integration)

# Deep Link Flow

URL scheme: livewire://join/VOUCHERCODE

# Example: Deep Link Validation


# Auth Endpoints

All auth endpoints require X-API-Key header (universal or per-client).

# Register

POST /auth/register
Body: { email, password, name?, voucherCode?, sessionId?, coachId?, profile?, settings? }
Response 201: { success, data: { user, tokens: { accessToken, refreshToken, expiresIn } } }
  • Universal key mode: client is resolved in this order:
    1. voucherCode provided -> use voucher's client (voucher is redeemed)
    2. No voucher -> fall back to the default client (set in admin portal)
    3. No voucher and no default client -> returns 400 NO_DEFAULT_CLIENT
  • Per-client key mode: voucherCode is optional (client from API key)
  • Email must be globally unique (one account per email across all clients)

# Login

POST /auth/login
Body: { email, password }
Response 200: { success, data: { user, tokens, client? } }
  • Universal key mode: user found globally by email; response includes client object with branding
  • Per-client key mode: user found within client scope; no client object in response

# Refresh Token

POST /auth/refresh
Body: { refreshToken }
Response 200: { success, data: { accessToken, expiresIn } }

No API key needed for refresh.

# Get Current User

GET /auth/me
Headers: Authorization: Bearer {accessToken}
Response 200: { success, data: { user, client? } }

Universal key mode includes client branding in the response.


# Token Management

// Store tokens securely (use expo-secure-store)
import * as SecureStore from 'expo-secure-store';

const storeTokens = async (tokens: { accessToken: string; refreshToken: string }) => {
  await SecureStore.setItemAsync('accessToken', tokens.accessToken);
  await SecureStore.setItemAsync('refreshToken', tokens.refreshToken);
};

// Create an API client with auto-refresh
const apiClient = async (path: string, options: RequestInit = {}) => {
  let token = await SecureStore.getItemAsync('accessToken');
  const apiKey = Config.UNIVERSAL_API_KEY; // or per-client key

  const res = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': apiKey,
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
  });

  if (res.status === 401) {
    // Try refresh
    const refreshToken = await SecureStore.getItemAsync('refreshToken');
    if (refreshToken) {
      const refreshRes = await fetch(`${API_BASE}/auth/refresh`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });
      if (refreshRes.ok) {
        const { data } = await refreshRes.json();
        await SecureStore.setItemAsync('accessToken', data.accessToken);
        // Retry original request
        return apiClient(path, options);
      }
    }
    // Refresh failed -> redirect to login
  }

  return res;
};

# Client Capabilities / Feature Gating

Each client has an enabledFeatures array. Use it to show/hide app sections.

Feature Key Description
classes On-demand class library
live_classes Live streaming classes
workout_builder AI workout generation and workout player
nutrition Nutrition tracking, food scanning, meal plans
coach_connect Connect with trainers/coaches
ai_coach AI coaching features
// After login or /auth/me, check features:
const features = client.enabledFeatures; // string[]
const hasLiveClasses = features.includes('live_classes');
const hasNutrition = features.includes('nutrition');

# Content Discovery (requires bearer token)

Content is automatically scoped to the user's client based on assigned categories.

# Home Screen

GET /content/discover
Response: { featured[], continueWatching[], upcomingLive[], categories[], newThisWeek[] }

# Browse Classes

GET /content/classes?page=1&limit=20&category=1&trainer=1&intensity=BEGINNER&sort=newest
Response: { classes[], pagination: { page, limit, total, pages } }

# Class Detail

GET /content/classes/:id
Response: { class with trainer, category, episodes, related classes }

# Search

GET /content/search?q=yoga&page=1&limit=20
Response: { results[], pagination }

# Categories

GET /content/categories
Response: { categories[] with class counts }

# Live Classes (requires bearer token)

GET  /live/schedule                  -- Upcoming live classes
POST /live/schedule/:classId         -- Add to user's schedule
DELETE /live/schedule/:classId       -- Remove from schedule
GET  /live/my-schedule               -- User's scheduled live classes

# Session Tracking (requires bearer token)

Track video viewing for "Continue Watching" and analytics.

POST /sessions/start         -- Body: { classId, totalDurationSeconds }
POST /sessions/:id/progress  -- Body: { progressSeconds, completionPercentage }
POST /sessions/:id/complete  -- Mark session complete
GET  /sessions/history       -- Viewing history with progress

# AI Services (requires bearer token)

POST /ai/workout-plan     -- Generate personalized workout plan
POST /ai/nutrition-plan   -- Generate nutrition plan
POST /ai/class-plan       -- Generate class recommendation plan

# Food Scanner (requires bearer token)

POST /food-scanner/scan    -- Body: multipart with image file
GET  /food-scanner/formats -- Supported image formats

# Onboarding Flow

For new users before registration:

POST /onboarding/save-profile     -- Save pending profile (returns sessionId)
POST /onboarding/recommend-coaches -- Get coach recommendations
POST /onboarding/select-coach      -- Select a coach

Then pass sessionId and coachId to /auth/register to link onboarding data.


# Trainers & Bookings (requires bearer token)

GET  /trainers                          -- List available trainers
GET  /trainers/:id                      -- Trainer details
GET  /trainers/:id/services             -- Trainer services and pricing
GET  /trainers/:id/availability         -- Available time slots
POST /trainers/:id/book                 -- Book one-on-one session
POST /trainers/:id/group-classes/:id/join -- Join group class
POST /trainers/:id/programmes/:id/subscribe -- Subscribe to programme
GET  /my-bookings/summary               -- Booking overview
GET  /my-bookings/sessions              -- List sessions

# LiveKit (Video Calls)

POST /livekit/token   -- Body: { roomName, participantName }
Response: { token, wsUrl }

Use @livekit/react-native to connect with the returned token and WebSocket URL.


# Error Handling

All error responses follow this structure:

{
  "error": "Human-readable message",
  "code": "MACHINE_READABLE_CODE",
  "details": "Additional context"
}

Common codes: MISSING_API_KEY, INVALID_API_KEY, INVALID_CREDENTIALS, USER_EXISTS, VOUCHER_REQUIRED, INVALID_VOUCHER, USER_NOT_FOUND, RATE_LIMIT_EXCEEDED.


# Rate Limiting

Response headers on client-scoped routes:

Header Description
X-RateLimit-Limit Max requests per hour
X-RateLimit-Remaining Requests remaining
X-RateLimit-Reset Reset timestamp (ISO 8601)

Handle 429 Too Many Requests with exponential backoff.


# Environment Configuration

// app.config.ts or env
export default {
  API_BASE_URL: process.env.EXPO_PUBLIC_API_BASE_URL || 'http://localhost:3010/api/v1',
  UNIVERSAL_API_KEY: process.env.EXPO_PUBLIC_UNIVERSAL_API_KEY || '',
  DEEP_LINK_SCHEME: 'livewire',
};

For deep linking in app.json:

{
  "expo": {
    "scheme": "livewire",
    "ios": { "bundleIdentifier": "com.livewire.app" },
    "android": { "package": "com.livewire.app" }
  }
}

# Key Integration Notes

  1. Always send X-API-Key on every request except /app/join/:code and /auth/refresh
  2. Bearer tokens expire in 15 minutes; refresh tokens last 7 days
  3. Content is client-scoped -- users only see classes in categories assigned to their client. If no categories are assigned, all content endpoints return empty arrays
  4. Emails are globally unique -- one user account per email, mapped to one client
  5. Voucher codes are case-insensitive and automatically uppercased by the backend
  6. The client object (with branding) is only included in universal key mode responses
  7. Default client fallback -- if a user registers without a voucher (e.g. organic signup), they are assigned to the admin-configured default client. The app should handle the NO_DEFAULT_CLIENT error gracefully if no default is configured

# Additional Resources

  • For exact request/response shapes, see the attached Postman collections:
    • FytOS_Client_API.postman_collection.json -- all user-facing endpoints
    • FytOS_Trainer_API.postman_collection.json -- trainer portal endpoints