StudiosTV OS / Livewire -- Expo App Integration
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.
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
clientIdclaim;/auth/mealways returnsdata.client.enabledFeatures
2. Per-Client Key (dedicated partner app)
Used when a partner has their own branded app with a dedicated API key.
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
Deep Link Flow
URL scheme: livewire://join/VOUCHERCODE
1. App receives deep link -> extract voucher code
2. GET /api/v1/app/join/{code} (no auth)
3. Response: { data: { valid, client: { id, name, logo, splashScreen, enabledFeatures } } }
4. If valid -> show client-branded onboarding/signup screen
5. User registers with POST /api/v1/auth/register (voucherCode in body)
6. Backend creates user mapped to voucher's client, redeems voucher
Example: Deep Link Validation
const validateDeepLink = async (code: string) => {
const res = await fetch(`${API_BASE}/app/join/${code}`);
const { data } = await res.json();
if (data.valid) {
// Show branded splash: data.client.splashScreen, data.client.logo
// Store code for registration
}
};
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:
voucherCodeprovided -> use voucher's client (voucher is redeemed)- No voucher -> fall back to the default client (set in admin portal)
- No voucher and no default client -> returns
400 NO_DEFAULT_CLIENT
- Per-client key mode:
voucherCodeis 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
clientobject with branding - Per-client key mode: user found within client scope; no
clientobject 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.
// 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
Video Access (requires bearer token)
Video URLs are never returned in class listing or detail endpoints. When the user taps play, request time-limited playback URLs:
GET /video/access/:classId -- Get playback URLs for a class
Response:
{
"success": true,
"data": {
"embedUrl": "https://player.vimeo.com/video/...",
"streamUrl": "https://player.vimeo.com/play/.../hls?s=...",
"streamExpiresAt": "2026-03-09T18:15:00Z",
"introEmbedUrl": null,
"introStreamUrl": null,
"introStreamExpiresAt": null,
"token": "<signed-jwt>",
"expiresAt": "2026-03-09T12:15:00Z",
"isLive": false
}
}
embedUrl-- Web: load in iframe via Vimeo Player SDK (domain-restricted)streamUrl-- Mobile (expo-video): direct HLS manifest URL. Pass touseVideoPlayer(streamUrl). Expires naturally (typically 6-12 hours)streamExpiresAt-- When the HLS URL expires. Request a fresh URL before expiry if the user is still watchingintroEmbedUrl/introStreamUrl-- Same as above for the intro video (if present)token/expiresAt-- Backend-signed JWT (15 min for on-demand, 2 hours for live)
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:
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
- Always send
X-API-Keyon every request except/app/join/:codeand/auth/refresh - Bearer tokens expire in 15 minutes; refresh tokens last 7 days
- 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
- Emails are globally unique -- one user account per email, mapped to one client
- Voucher codes are case-insensitive and automatically uppercased by the backend
- The
clientobject (with branding) is only included in universal key mode responses - 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_CLIENTerror 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 (StudiosTV OS Client API)FytOS_Trainer_API.postman_collection.json-- trainer portal endpoints (StudiosTV OS Trainer API)