# AI Chat Source: https://docs.launchtoday.dev/ai-features/overview Build intelligent conversational experiences with multi-provider AI support AI Chat Interface ## Overview Launch includes a production-ready AI chat feature with streaming responses and chat history persistence. The architecture is modular, making it easy to extend with additional providers and custom system prompts. ## Prerequisites * Backend running with AI env vars set (copy from `apps/api/example.env` first) * `EXPO_PUBLIC_API_URL` set in `apps/mobile/.env` (from `apps/mobile/example.env`) This feature is gated by the mobile feature registry flag: `ai`. See [Feature Registry](/mobile/feature-registry). ## Key Features OpenAI is enabled by default. Anthropic support is wired in the backend and can be enabled in the mobile model list with a single config change. Real-time streaming for a responsive chat experience with stop generation support. Predefined personas and custom system prompts to shape AI behavior. Conversation history stored in PostgreSQL via tRPC procedures. ## Architecture The AI feature is built with extensibility in mind, separating concerns into distinct layers. ```mermaid theme={null} graph TB A[Chat Screen] --> B[ChatContext] B --> C[useChat Hook] B --> D[useChatHistory Hook] B --> E[useChatPersistence Hook] C --> F[AI Provider Layer] F --> G[OpenAI] F --> H[Anthropic] F --> I[Custom Providers] E --> J[tRPC API] J --> K[Database] ``` ### Core Components **Location**: `lib/ai/providers/` A unified interface for working with different AI providers. OpenAI is enabled by default; add Anthropic to the providers list to expose Claude models in the UI. **Location**: `features/chat/hooks/` Business logic extracted into reusable hooks: `useChat` for messaging, `useChatHistory` for history management, and `useChatPersistence` for database sync. **Location**: `features/chat/context/` Global state management for chat functionality, reducing prop drilling and simplifying component interactions. ## Steps 1. Copy the example env (if not already): ```bash theme={null} cd apps/api cp example.env .env ``` 2. Add API keys to your backend `.env`: ```bash theme={null} # OpenAI (required for AI chat) OPENAI_API_KEY=sk-... # Anthropic (optional - enable in mobile model list) ANTHROPIC_API_KEY=sk-ant-... ``` 3. Ensure the AI feature flag is enabled: `apps/mobile/features/feature-registry.tsx` → `featureFlags.ai = true` 4. (Optional) Enable Anthropic models in the mobile picker: `apps/mobile/lib/ai/providers/index.ts` → add `anthropicProvider` to the `providers` array. 5. Open the chat screen at `/ai-chat`. ## How It Works * Mobile streams tokens from the API via `POST /api/ai/stream` (SSE) * Chat history is persisted via tRPC procedures under `chat.*` * System prompts are defined in `apps/mobile/lib/ai/prompts/index.ts` ## Key Files * API streaming: `apps/api/src/routes/ai-stream.ts` * AI providers: `apps/api/src/services/openai.ts` and `apps/api/src/services/anthropic.ts` * Chat persistence: `apps/api/src/routers/chat.ts` * Mobile streaming client: `apps/mobile/lib/api/streaming.ts` * Chat UI and hooks: `apps/mobile/features/chat/` ## Customizing System Prompts System prompts define the AI's persona and behavior: ```typescript theme={null} export const systemPrompts = { staffEngineerMentor: { id: "staff-engineer-mentor", name: "Staff Engineer Mentor", description: "A senior mentor who teaches through the Socratic method", prompt: `You are a senior staff software engineer with 20+ years...`, }, codingAssistant: { id: "coding-assistant", name: "Coding Assistant", description: "A helpful coding assistant", prompt: `You are a helpful coding assistant...`, }, }; ``` Conversations are persisted in Prisma under the `Chat` and `ChatMessage` models in `apps/api/prisma/schema.prisma`. ## API Endpoints The chat feature exposes the following tRPC procedures: | Procedure | Type | Description | | ------------------ | -------- | ---------------------------------- | | `chat.list` | Query | Get all chats for the current user | | `chat.get` | Query | Get a specific chat with messages | | `chat.create` | Mutation | Create a new chat | | `chat.delete` | Mutation | Delete a chat | | `chat.addMessage` | Mutation | Add a message to a chat | | `chat.updateTitle` | Mutation | Update chat title | ## Test Checklist * Chat screen loads and streams responses * New chats are saved and appear in history * Switching models updates the active provider ## Troubleshooting If streaming fails, verify API keys and server logs. For general issues, start with [Troubleshooting](/troubleshooting). ## Remove / Disable To disable AI while you configure providers, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.ai = false` For production removal guidance, see [Removing Features](/essentials/removing-features). ## Next Steps * [Feature Registry](/mobile/feature-registry) * [Incident Debugging](/essentials/incident-debugging) # App Store Submission Source: https://docs.launchtoday.dev/app-stores/index Get your app approved and published on iOS and Android ## Overview Submitting to the App Store and Google Play can be daunting. This guide walks you through the entire process, from preparation to approval. Launch is Expo-first, so use EAS Submit for both stores. For now, use the checklist and asset requirements below, plus the official Apple/Google submission docs for platform‑specific details. ## Prerequisites * Production build ready * Privacy policy + support URL * App metadata (title, subtitle, keywords) ## Steps ## Short Path (Expo) 1. Create production builds with EAS 2. Submit with `eas submit` 3. Respond to store review feedback ## Pre-Submission Checklist 1024x1024 PNG for iOS, 512x512 for Android Required sizes for each device type Title, subtitle, description, keywords Required URL to your privacy policy Contact or support page URL ## Required Assets ### iOS Screenshots | Device | Size | Required | | -------------- | ----------- | ----------------- | | iPhone 6.7" | 1290 x 2796 | Yes | | iPhone 6.5" | 1284 x 2778 | Yes | | iPhone 5.5" | 1242 x 2208 | Optional | | iPad Pro 12.9" | 2048 x 2732 | If iPad supported | ### Android Screenshots | Type | Size | Required | | ---------- | ------------ | ------------------- | | Phone | 1080 x 1920+ | Yes (2-8 images) | | Tablet 7" | 1080 x 1920+ | If tablet supported | | Tablet 10" | 1080 x 1920+ | If tablet supported | ## Common Rejection Reasons Avoid these common pitfalls that lead to App Store rejection: Your app must be fully functional. No placeholder screens or "coming soon" features. A privacy policy URL is required. Must be accessible and comprehensive. Provide demo account credentials for review if login is required. Test thoroughly before submission. Any crash will result in rejection. Screenshots must match actual app. No misleading descriptions. ## Troubleshooting * **Rejected for incomplete functionality**: remove placeholder screens and test critical flows * **Metadata mismatch**: ensure screenshots and descriptions match the app ## Timeline | Store | Initial Review | Updates | | ------------- | -------------- | -------- | | iOS App Store | 1-7 days | 1-3 days | | Google Play | 1-3 days | 1-2 days | Submit your app early in the week (Monday/Tuesday) for faster review times. ## Next Steps * Create a build with EAS: [Mobile Deployment with EAS](/deployment/mobile-eas) * Review security readiness: [Security Checklist](/security/checklist) # Apple Sign-In Setup Source: https://docs.launchtoday.dev/authentication/apple-signin Complete guide to configuring Apple Sign-In for iOS apps ## Overview Apple Sign-In provides a secure and privacy-focused authentication method for iOS users. The Launch boilerplate already includes the Apple Sign-In wiring on both the API and the mobile client—you just need to enable it and add your credentials. ## Prerequisites * **Apple Developer Account** (paid membership required) * **iOS App** with unique Bundle Identifier * **Access to Apple Developer Console** Start with [Backend Authentication Setup](/authentication/backend-setup) so your API and env file are in place. ## Steps ## Step 0: Set Up ngrok (Recommended First) Apple and Google sign-in flows work best with a stable HTTPS callback URL. Use ngrok to expose your local API, then set that URL in both the backend and mobile env files. Run ngrok and copy the HTTPS forwarding URL: ```bash theme={null} ngrok http 3001 ``` Example forwarding URL: ``` https://27f9fd215cd7.ngrok-free.app ``` 1. Start a tunnel to your API (running on port 3001). 2. Copy the HTTPS URL from ngrok. 3. Update `apps/api/.env` with `BETTER_AUTH_URL` set to the ngrok URL. 4. Update `apps/mobile/.env` with `EXPO_PUBLIC_API_URL` set to the same ngrok URL. 5. Restart the API and the mobile app so the new URLs are used. ## Step 1: Create or Configure Your App ID 1. Log in to [Apple Developer Console](https://developer.apple.com) 2. Select **Account** from the top navigation 3. Choose **Certificates** 4. Pick your App ID (or create one) and set a bundle identifier such as `com.company.example` 5. Scroll down, find **Sign in with Apple**, enable it, and make sure **Enable as a primary App ID** is checked 6. Click **Save** ## Step 2: Generate a Private Key 1. Select **Account** from the top navigation 2. Open **Keys** 3. Click the **+** button 4. Enter a **Key Name** (e.g., "Launch Apple Sign-In") 5. Find **Sign in with Apple** and click **Configure** 6. Choose your **Primary App ID** (this should be the same bundle ID you set in Step 1, part 4) 7. Click **Save** 8. Click **Continue**, then **Register** 9. Download the `.p8` key immediately (you can only download it once) 10. Note the **Key ID** and your **Team ID** for the next steps The `.p8` file can only be downloaded once. Store it securely and never commit it to version control. ## Step 3: Convert the Private Key to Base64 Apple requires the `.p8` file to be stored as a single-line base64 string in your environment. Convert the key locally using a secure method you trust, and avoid online converters. Example command: ``` base64 -i AuthKey_KEYID.p8 | tr -d '\n' ``` ## Step 4: Update Your API Environment Add the values below to your backend environment file (see `apps/api/.env` for where they live). The backend reads these at startup to enable Apple Sign-In. After updating the API environment, restart the API server so the new values are loaded. Then restart the mobile app by running `pnpm prebuild`, followed by `pnpm ios` to launch the iOS app. You will need the following values: * `APPLE_CLIENT_ID` (your iOS bundle identifier) * `APPLE_TEAM_ID` (your 10-character team ID) * `APPLE_KEY_ID` (your 10-character key ID) * `APPLE_PRIVATE_KEY_BASE64` (the base64 version of the `.p8` key) ### Finding Your Team ID 1. Go to [Apple Developer Console](https://developer.apple.com) 2. Click on your name in the top right 3. Your **Team ID** is displayed next to your name ### Finding Your Bundle Identifier 1. In Apple Developer Console, go to **Identifiers** → **App IDs** 2. Select your app 3. The **Identifier** field shows your Bundle ID ## Step 5: Mobile App Configuration Update `apps/mobile/app.config.ts` to: * Set your iOS bundle identifier (this must match `APPLE_CLIENT_ID`). * Uncomment the `expo-apple-authentication` plugin. * Enable the Apple Sign-In entitlement when you are ready to ship. ## Step 6: Backend Configuration The boilerplate already includes the server-side Apple Sign-In wiring in `apps/api/src/services/auth.ts`. Once the Apple environment values exist, the provider is enabled automatically. ## Step 7: Testing Confirm Apple Sign-In is working by: 1. Running the iOS app on a simulator or device. 2. Tapping the Apple Sign-In button on the login screen. 3. Verifying that you return to the app authenticated. ### Success Checks * You can sign in successfully from the app using Apple. * Query your database and confirm a new user appears in the `users` table and a linked record exists in the `accounts` table. ## Troubleshooting * **Apple Sign-In button not appearing**: ensure you are running on iOS and that `expo-apple-authentication` is enabled in `apps/mobile/app.config.ts`. * **Invalid client**: confirm your bundle ID matches `APPLE_CLIENT_ID`. * **Backend validation errors**: verify all Apple env values are set and restart the API after changes. # Backend Authentication Setup Source: https://docs.launchtoday.dev/authentication/backend-setup Configure Better Auth on the API backend for secure authentication ## Overview The Launch API backend uses [Better Auth](https://better-auth.com) to handle authentication for both the server and the mobile client. It provides a unified flow for sessions, social sign-in, and account management, so the API and app stay in sync. ## How It Works (High Level) Better Auth runs in the API and exposes authentication endpoints that the mobile app calls. When a user signs in, the API issues a session and persists user data in the database. The mobile app then uses that session to access protected API routes. ## What You Configure You will set up: * A database connection (so auth can store users and sessions). * A strong auth secret (used to sign and validate sessions). * Optional provider credentials (Apple, Google, etc.) when you enable them. * Trusted origins for development and production. ## What’s Required To Run Locally At minimum, the API needs a database URL and a Better Auth secret to boot. Other provider values can stay unset until you start integrating those features. ## Server-Side Authentication Flow The API wiring lives in a small set of files: * `apps/api/src/services/auth.ts` initializes Better Auth, enables Expo + email OTP plugins, and conditionally enables Apple/Google providers when their credentials exist. It also sets session lifetimes and cookie behavior and captures device metadata on sign-in. * `apps/api/src/routes/auth/auth.ts` bridges Koa requests to the Better Auth handler, logs auth requests safely, and stores device details on successful sign-in. * `apps/api/src/services/device-middleware.ts` writes device metadata to the session record, using the token returned by Better Auth. ## Mobile Client Authentication Flow On the client, authentication is structured around a dedicated auth client and screen-specific flows: * `apps/mobile/lib/auth/client.ts` creates the Better Auth client, sets the API base URL, adds Expo + email OTP plugins, and injects device headers on every auth call. It also stores auth data securely using Expo Secure Store. * `apps/mobile/lib/auth/session-context.tsx` provides a session context with a single fetch-on-mount flow, plus `refetch` and `clearSession` helpers. * `apps/mobile/app/auth/login.tsx` drives Apple/Google sign-in and refreshes the session after successful auth. * `apps/mobile/app/auth/email-signin.tsx` requests an email OTP. * `apps/mobile/app/auth/verify-email-otp.tsx` verifies the OTP and refreshes the session on success. * `apps/mobile/lib/auth/google.ts` handles Google OAuth via Expo Auth Session, does the code exchange, and returns an ID token that Better Auth can validate. * `apps/mobile/lib/trpc/client.ts` attaches Better Auth cookies to API requests so authenticated calls work with tRPC. ## Next Steps Start with Apple Sign-In if you are shipping iOS. If you’re focused on Android first, skip ahead to Google Sign-In and come back to Apple later. * [Apple Sign-In (iOS)](/authentication/apple-signin) * [Google Sign-In (Android)](/authentication/google-signin) # Email Sign-in with OTP Source: https://docs.launchtoday.dev/authentication/email-signin Set up passwordless email authentication using one-time passwords (OTP) ## Overview Email sign-in is fully wired with Better Auth and Resend. Users enter their email, receive a 6-digit one-time password (OTP), and verify it in-app to create a session. Resend is the email delivery service used by the boilerplate to send OTP codes. OTP is a short, time-limited code that lets users sign in without a password. The API generates the code, sends it via Resend, and verifies it when the user enters it on mobile. ## Prerequisites * A Resend account * A Resend API key * Backend environment configured Start with [Backend Authentication Setup](/authentication/backend-setup) so your API and env file are in place. ## How OTP works 1. The user enters their email in the app. 2. The API generates an OTP and sends it via Resend. 3. The user enters the OTP, and the API verifies it. 4. Better Auth creates the session and the app becomes authenticated. ## Where it is configured The boilerplate already includes the email OTP wiring: * Backend auth service and Resend send logic: `apps/api/src/services/auth.ts` * Better Auth request handling: `apps/api/src/routes/auth/auth.ts` * Mobile screens for email input and OTP verification: `apps/mobile/app/auth/email-signin.tsx`, `apps/mobile/app/auth/verify-email-otp.tsx` * Session handling on mobile: `apps/mobile/lib/auth/session-context.tsx` ## Setup steps ### Step 1: Create a Resend API key Sign in to [Resend](https://resend.com/), then create an API key from the [API Keys](https://resend.com/api-keys) page. ### Step 2: Add the API key to your backend env Copy `apps/api/example.env` to `apps/api/.env` if you have not already, then set `RESEND_API_KEY` to your Resend API key. Restart the API after updating backend environment values. ### Step 3: Verify your sending domain (production) For production email delivery, verify your domain in [Resend Domains](https://resend.com/domains) and follow their DNS instructions. The sender address is defined in `apps/api/src/services/auth.ts`. Update it to use an address on your verified domain. ### Step 4: Test email sign-in Open the app, enter an email address, and request a code. You should receive an OTP email and be able to complete the sign-in flow. ## Notes * If you are not receiving emails in development, confirm your Resend API key and check spam folders. * If OTP verification fails, confirm the API is running and reachable from the device, and restart it after env changes. # Google Sign-In Source: https://docs.launchtoday.dev/authentication/google-signin Configure Google Sign-In for the Launch mobile app ## Overview This guide walks you through creating Google OAuth credentials and connecting them to the Launch mobile app. The boilerplate uses Expo AuthSession in `apps/mobile/lib/auth/google.ts` and sends the Google token to the API for verification. These steps work for both iOS and Android in the Launch template. ## Prerequisites * Access to Google Cloud Console * A Google Cloud project created for your app Start with [Backend Authentication Setup](/authentication/backend-setup) so your API and env file are in place. ## Steps ## Step 1: Create your OAuth client ID (iOS) Open [Google Cloud Console](https://console.cloud.google.com/), then go to **APIs & Services** → **Credentials** and click **Create credentials** → **OAuth client ID**. If Google prompts you to configure the **OAuth consent screen** first, complete that setup before continuing. Choose **External**, enter your app name, support email, and developer contact email, then add yourself as a test user. Save the changes and return to the **Credentials** page. Now create an **iOS** OAuth client. Give it a name, then enter the iOS bundle identifier from `apps/mobile/app.config.ts`. After you create the client, copy the iOS client ID—you will use it in your mobile environment file. ## Step 2: Create a Web client ID (required) The shared Google Auth module validates both an iOS client ID and a Web client ID, even when you are testing mobile locally. Create a **Web application** OAuth client and use your ngrok HTTPS URL for both the origin and redirect. Set these values in the Web client configuration (replace with your ngrok forwarding URL): * **Authorized JavaScript origins**: `https://YOUR_NGROK_ID.ngrok-free.app` * **Authorized redirect URIs**: `https://YOUR_NGROK_ID.ngrok-free.app/api/auth/callback/google` These should match the ngrok URL you set in your backend and mobile env files so the OAuth flow stays on a single origin. ## Step 3: Update environment files Update `apps/mobile/.env` with: * `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID` * `EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID` Update `apps/api/.env` with: * `GOOGLE_WEB_CLIENT_ID` * `GOOGLE_CLIENT_SECRET` Restart the API after changing backend values. For the mobile app, stop your dev server and run `pnpm ios` or `pnpm android` again so the updated env values are bundled into the development build. You only need to run `pnpm prebuild` again if you changed native config in `apps/mobile/app.config.ts` (for example, URL schemes). ## Step 4: Enable iOS deep links Update `apps/mobile/app.config.ts` to enable the iOS URL scheme used by Google Sign-In. Set the `CFBundleURLName` to a readable app name, and set the `CFBundleURLSchemes` value to the **iOS URL scheme** shown in Google Cloud Console for the iOS client you created in Step 1. If these values are currently commented out, uncomment them and replace the placeholders. When you set `CFBundleURLName`, it is safest to use the same value as your bundle identifier (for example, `com.company.launchstarter`) so it stays consistent with the rest of your iOS configuration. ## Step 5: Test the sign-in flow Open the app, tap Google Sign-In, and complete the consent flow. You should be redirected back into the app and see an authenticated session. # Auth Troubleshooting Source: https://docs.launchtoday.dev/authentication/troubleshooting Common OAuth and session issues with fixes ## Prerequisites * API running locally or deployed * `BETTER_AUTH_URL` and `EXPO_PUBLIC_API_URL` aligned ## Steps 1. Identify the failing provider (Apple, Google, or Email OTP) 2. Confirm the API is reachable from the device 3. Check API logs for the request 4. Verify env vars and callback URLs ## Troubleshooting * **OAuth state mismatch**: ensure API and mobile use the same base URL. Mixing `http://192.168.x.x:3001` on mobile with an ngrok URL on the API will fail. * **Invalid client**: confirm `GOOGLE_WEB_CLIENT_ID` (backend) and `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID` / `EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID` (mobile) match the credentials you created. * **Provider not found**: the API only enables Google or Apple if the required env vars are present. Double-check `GOOGLE_WEB_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and the Apple env values, then restart the API. * **Callback 404**: check `/api/auth/callback/*` routes are reachable and that `BETTER_AUTH_URL` is set to the same origin used by the device. * **Session not persisting**: confirm you are using a development build (not Expo Go) and that SecureStore is available on the device. * **Email OTP not sending**: verify `RESEND_API_KEY` and that your sender domain is verified in Resend for production. ## Need help? If you run into any auth issues you cannot resolve, email `support@launchtoday.dev` with: * Which provider failed (Apple, Google, Email OTP) * The exact error message or screenshot * Whether you are testing locally or on a deployed API * The device and platform (iOS simulator, Android device, etc.) # Database ORM Source: https://docs.launchtoday.dev/backend/database-orm Switch between Prisma and Drizzle Launch ships with a thin ORM adapter layer so you can swap Prisma or Drizzle without rewriting app code. You should pick **one** ORM for a project and remove the other to keep the dependency tree and docs clean. ## Choose an ORM Set `DATABASE_ORM` in `apps/api/.env`: ```bash theme={null} DATABASE_ORM=prisma ``` ```bash theme={null} DATABASE_ORM=drizzle ``` ## Prisma (default) * Uses `prisma/schema.prisma` and Prisma migrations. * `db:generate`, `db:migrate`, and `db:push` run Prisma commands when `DATABASE_ORM=prisma`. ## Drizzle + Neon * Uses Drizzle ORM with the Neon serverless driver (and `pg` for local Postgres). * You can reuse the same Postgres schema; the adapter targets the same tables. * Drizzle is **code-first**: the `schema.ts` file defines tables, and Drizzle Kit generates SQL migrations from it. ### Drizzle workflow 1. Edit `apps/api/src/lib/db/schema.ts` 2. Generate SQL migrations: `pnpm --filter api db:generate` 3. Apply to the database: `pnpm --filter api db:push` ## Remove the ORM you don’t use After you pick one ORM, delete the other to avoid confusion and remove the unused dependencies from `apps/api/package.json`. ### If you choose Prisma, remove Drizzle 1. Delete Drizzle files: * `apps/api/src/lib/db/drizzle-adapter.ts` * `apps/api/src/lib/db/schema.ts` * `apps/api/drizzle.config.ts` * `apps/api/drizzle/` 2. Remove Drizzle dependencies from `apps/api/package.json`: * `drizzle-orm` * `drizzle-kit` * `@neondatabase/serverless` * `ws` + `@types/ws` 3. Update `apps/api/src/lib/db/index.ts` to only load Prisma. ### If you choose Drizzle, remove Prisma 1. Delete Prisma files: * `apps/api/prisma/` * `apps/api/src/lib/db/prisma-adapter.ts` * `apps/api/src/generated/` 2. Remove Prisma dependencies from `apps/api/package.json`: * `prisma` * `@prisma/client` * `@prisma/adapter-pg` 3. Update `apps/api/src/lib/db/index.ts` to only load Drizzle. 4. Update the build script to remove `prisma generate`. ## Notes * `db:generate`, `db:migrate`, and `db:push` auto-select Prisma or Drizzle based on `DATABASE_ORM` in `apps/api/.env`. * `db:reset` runs Prisma reset. For Drizzle, use manual SQL or a dedicated reset workflow. * For Neon setup and safe migration guidance, see [Neon Database](/backend/neon). * Prisma and Drizzle migrations are separate workflows. Pick one toolchain per project and keep it consistent. * Switching the ORM does not change your database; it only changes the client. # Environment Variables Source: https://docs.launchtoday.dev/backend/environment-variables Configure your backend environment ## Prerequisites * Know which features you plan to enable * Access to your deployment environment for setting secrets ## Steps 1. Copy `apps/api/example.env` to `apps/api/.env` 2. Add required base vars (`DATABASE_URL`, `BETTER_AUTH_SECRET`) 3. Add provider vars only for enabled features 4. Restart the API server to load changes ## Required Variables (Base API) | Variable | Description | Example | | -------------------- | ---------------------------------------------- | ------------------------------------- | | `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` | | `DATABASE_ORM` | ORM adapter (`prisma` or `drizzle`) | `prisma` | | `BETTER_AUTH_SECRET` | Secret for session encryption (32+ chars) | `long-random-string` | | `BETTER_AUTH_URL` | API base URL for OAuth callbacks (recommended) | `http://localhost:3001` | | `MOBILE_APP_URL` | App URL scheme for mobile auth origins | `launch://` | ## Server & Logging | Variable | Description | Default | | ----------- | --------------------------------------------- | ------------- | | `PORT` | API server port | `3001` | | `HOST` | API server host | `localhost` | | `NODE_ENV` | Environment (`development`/`production`) | `development` | | `LOG_LEVEL` | Logging level (`debug`/`info`/`warn`/`error`) | `info` | ## Authentication Providers ### Apple Sign In | Variable | Description | | -------------------------- | ----------------------------------------- | | `APPLE_CLIENT_ID` | Apple Services ID (matches iOS bundle ID) | | `APPLE_TEAM_ID` | Apple Developer Team ID | | `APPLE_KEY_ID` | Apple Key ID | | `APPLE_PRIVATE_KEY` | Private key contents (PEM) | | `APPLE_PRIVATE_KEY_BASE64` | Base64-encoded private key (alternative) | > Use **either** `APPLE_PRIVATE_KEY` or `APPLE_PRIVATE_KEY_BASE64`. ### Google Sign In | Variable | Description | | ---------------------- | ------------------- | | `GOOGLE_WEB_CLIENT_ID` | OAuth web client ID | | `GOOGLE_CLIENT_SECRET` | OAuth client secret | ## Email & SMS | Variable | Description | | ------------------------------ | -------------------------------------------------------------------- | | `RESEND_API_KEY` | Resend API key (email OTP) | | `TWILIO_ACCOUNT_SID` | Twilio account SID (SMS OTP) | | `TWILIO_AUTH_TOKEN` | Twilio auth token | | `TWILIO_SERVICE_SID` | Twilio Verify service SID | | `SKIP_TWILIO_OTP_VERIFICATION` | Skip SMS verification in dev (`true`/`false`, ignored in production) | ## Payments (Stripe) | Variable | Description | | ------------------------ | ----------------------------- | | `STRIPE_SECRET_KEY` | Stripe API secret key | | `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key | | `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | ## AI Providers | Variable | Description | | --------------------- | --------------------------------- | | `OPENAI_API_KEY` | OpenAI API key | | `OPENAI_ORGANIZATION` | OpenAI organization ID (optional) | | `ANTHROPIC_API_KEY` | Anthropic API key (optional) | ## File Uploads (S3) | Variable | Description | Default | | ---------------------- | --------------------- | ----------- | | `S3_BUCKET` | S3 bucket name | (none) | | `S3_REGION` | AWS region | `us-east-1` | | `S3_ACCESS_KEY_ID` | AWS access key ID | (none) | | `S3_SECRET_ACCESS_KEY` | AWS secret access key | (none) | ## Push Notifications ### APNS (iOS) | Variable | Description | | ----------------- | ------------------------------------- | | `APNS_TEAM_ID` | Apple Team ID | | `APNS_KEY_ID` | APNS key ID | | `APNS_BUNDLE_ID` | iOS bundle identifier | | `APNS_PRODUCTION` | `true` for production APNS | | `APNS_KEY` | APNS private key contents (PEM) | | `APNS_KEY_BASE64` | Base64-encoded APNS key (alternative) | ### FCM (Android) | Variable | Description | | ---------------------------- | ----------------------------------- | | `FCM_PROJECT_ID` | Firebase project ID | | `FCM_SERVICE_ACCOUNT_BASE64` | Base64-encoded service account JSON | ### Push Campaign API | Variable | Description | | ------------------------- | ------------------------------- | | `PUSH_TOKEN_ADMIN_SECRET` | Bearer token for push campaigns | ## Notes * The API reads env vars directly at startup. Missing required values will cause runtime errors, so prefer setting envs before boot. * If you don’t need a feature, you can leave its env vars unset and disable the corresponding mobile feature flag. * `DATABASE_ORM=drizzle` expects a Postgres-compatible database (Neon works well). ## Troubleshooting * **Server fails on boot**: confirm required vars exist * **OAuth callbacks fail**: verify `BETTER_AUTH_URL` * **File uploads fail**: check S3 vars and bucket permissions ## Next Steps * [Backend Overview](/backend/index) * [Authentication Setup](/authentication/backend-setup) # Backend Overview Source: https://docs.launchtoday.dev/backend/index Server-side API and deployment options The Launch backend is a lightweight Node server designed for mobile-first products: authentication, payments, uploads, and a type-safe API surface for the app. ## Prerequisites * PostgreSQL database available * `.env` values configured for the features you will use ## Steps 1. Configure env vars: [Environment Variables](/backend/environment-variables) 2. Choose ORM: [Database ORM](/backend/database-orm) 3. Provision a DB (optional): [Neon Database](/backend/neon) 4. Run migrations in `apps/api` 5. Start the server with `pnpm dev` ## Tech Stack Minimal Node.js server framework. Easy to understand and customize. Switchable ORM adapters for PostgreSQL (Prisma default, Drizzle optional). Reliable, production-ready relational database. End-to-end typesafe APIs without code generation. ## What's Included * **Authentication API** - Session management with Better Auth * **User Management** - Profile storage and updates * **Payment Webhooks** - Stripe webhook handlers * **Database Migrations** - Prisma schema + migrations (or Drizzle tooling) * **Type Safety** - Shared `AppRouter` types for mobile + web ## File Structure ``` apps/api/ ├── src/ │ ├── routers/ # tRPC routers (chat, stripe, upload, user) │ ├── routes/ # REST endpoints (auth, webhooks, ai streaming) │ ├── services/ # Business logic (auth, AI, payments) │ ├── lib/ # ORM adapters, S3, Stripe clients │ ├── config/ # Env/config │ ├── app.ts # Koa app wiring │ └── index.ts # Server entry point ├── prisma/ # schema.prisma + migrations └── package.json ``` ## Deployment Options The backend can be deployed to various platforms: | Platform | Type | Best For | | -------- | ---------- | -------------------------------- | | Railway | Container | Simple deploys, auto-scaling | | Vercel | Serverless | Edge functions, fast cold starts | | Fly.io | Container | Global distribution | | AWS | Various | Enterprise, custom infra | ## Troubleshooting * **Database connection errors**: confirm `DATABASE_URL` * **Auth provider errors**: verify provider env vars ## Next Steps Deploy to Railway in minutes Configure your backend # Neon Database Source: https://docs.launchtoday.dev/backend/neon Provision and connect a Neon Postgres database Use Neon when you want a managed Postgres database for Launch. ## Create a Neon database 1. Create a project in Neon. 2. Copy the **connection string** from the Neon dashboard. 3. Set it as `DATABASE_URL` in `apps/api/.env`: ```bash theme={null} DATABASE_URL=postgresql://user:pass@host.neon.tech/db ``` ## Apply migrations After setting `DATABASE_URL`, run the same migration commands you use locally: ```bash theme={null} pnpm --filter api db:generate pnpm --filter api db:migrate pnpm --filter api db:push ``` ## Run migrations safely Be careful running migrations against production from your laptop. Recommended safety practices: * Run migrations from CI (GitHub Actions) instead of local machines. * Apply migrations only after a successful deploy. * Use a dedicated “migration” step with environment‑scoped credentials. * Test migrations on a staging database first. If you need more control, move to a CI workflow that runs migrations with explicit approvals and environment protection rules. # Railway Deployment (Coming Soon) Source: https://docs.launchtoday.dev/backend/railway-deployment Deploy your backend to Railway This guide is coming soon. It will cover deploying the Launch backend to Railway with PostgreSQL. ## What is Railway? Railway is a modern deployment platform that makes it easy to deploy applications with: * **One-click deploys** from GitHub * **Managed PostgreSQL** databases * **Automatic HTTPS** and custom domains * **Environment variables** management * **Auto-scaling** based on traffic ## Coming Soon This guide will cover: Sign up at [railway.app](https://railway.app) Link your Launch repository to Railway Provision a managed PostgreSQL instance Set up required environment variables: - `DATABASE_URL` - `BETTER_AUTH_SECRET` - `STRIPE_SECRET_KEY` - etc. Push to main branch to trigger deployment Apply database migrations ## Quick Reference ```bash theme={null} # Environment variables needed DATABASE_URL=postgresql://... BETTER_AUTH_SECRET=your-secret-key BETTER_AUTH_URL=https://your-app.railway.app # Stripe (if using) STRIPE_SECRET_KEY=sk_live_... STRIPE_WEBHOOK_SECRET=whsec_... # OAuth (if using) GOOGLE_WEB_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... APPLE_CLIENT_ID=... APPLE_CLIENT_SECRET=... ``` ## Resources Official Railway documentation Pre-configured templates *** Full deployment guide coming soon. In the meantime, Railway's documentation provides excellent getting-started guides. # Deployment Source: https://docs.launchtoday.dev/deployment/index Deploy your app to production - backend, mobile, and beyond ## Overview Launch is designed for easy deployment to production. This section covers deploying both your backend API and mobile app. Most of the mobile workflow is handled by Expo (EAS Build + EAS Submit), so deployment can be completed in a single afternoon once your backend is live. Deploy your API to Railway in minutes. Build and distribute with Expo Application Services. If you need Docker or advanced env management, use the official Railway docs or your platform’s deployment guides. ## Prerequisites * A production database * A hosting provider for the API * Apple/Google developer accounts (if submitting to stores) ## Steps 1. Deploy the API (Railway or your provider) 2. Set production env vars (API + mobile) 3. Build mobile binaries with EAS 4. Submit to the stores ## Deployment Checklist Before deploying to production, ensure you've completed: In `apps/api/src/config/env.ts`, update any integration env vars you rely on (Stripe, Apple, Google, Twilio, push, AWS, etc.) from optional to required so the API fails fast if a value is missing. All production environment variables configured Production database created and migrated Completed [security checklist](/security/checklist) All critical flows tested on staging ## Quick Links | Platform | Time | Difficulty | | ------------------ | ------ | ---------- | | Railway (Backend) | 10 min | Easy | | EAS Build (Mobile) | 15 min | Easy | | EAS Submit | 20 min | Medium | **First time deploying?** Start with [Backend to Railway](/backend/railway-deployment) - it's the fastest way to get your API live. ## Troubleshooting * **Env missing at runtime**: verify variables in your host (Railway/EAS) * **Mobile can’t reach API**: confirm `EXPO_PUBLIC_API_URL` ## Next Steps * [Mobile Deployment with EAS](/deployment/mobile-eas) * [Security Checklist](/security/checklist) # Mobile Deployment with EAS Source: https://docs.launchtoday.dev/deployment/mobile-eas Build and distribute your app with Expo Application Services ## Overview Expo Application Services (EAS) handles building and submitting your app to the App Store and Google Play. For most apps, EAS is the only deployment workflow you need. ## Prerequisites * Expo account ([expo.dev](https://expo.dev)) * EAS CLI installed: `npm install -g eas-cli` * Apple Developer account (for iOS) * Google Play Developer account (for Android) ## Steps ## 1. Configure EAS ```bash theme={null} cd apps/mobile # Login to Expo eas login # Configure EAS for your project eas build:configure ``` This creates `eas.json`: ```json theme={null} { "cli": { "version": ">= 5.0.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal" }, "production": {} }, "submit": { "production": {} } } ``` ## Development Builds (Required for Native SDKs) Some integrations (RevenueCat, push notifications, background uploads) require a development build and will not work in Expo Go. Create a dev client: ```bash theme={null} eas build --platform ios --profile development eas build --platform android --profile development ``` ## 2. Build for iOS ```bash theme={null} # Development build (with dev client) eas build --platform ios --profile development # Production build (for App Store) eas build --platform ios --profile production ``` ## 3. Build for Android ```bash theme={null} # Development build eas build --platform android --profile development # Production build (for Play Store) eas build --platform android --profile production ``` ## 4. Submit to Stores ```bash theme={null} # Submit iOS build to App Store Connect eas submit --platform ios # Submit Android build to Google Play eas submit --platform android ``` ## Environment Variables For production builds, configure environment variables in `eas.json`: ```json theme={null} { "build": { "production": { "env": { "EXPO_PUBLIC_API_URL": "https://your-api.railway.app" } } } } ``` ## Troubleshooting **iOS**: Run `eas credentials` to manage certificates **Android**: Ensure keystore is properly configured EAS builds typically take 10-20 minutes. Check build logs for specific steps. Check that all environment variables are set correctly for production. ## Next Steps Submission checklist and required assets Final pre-launch review # Error Handling & Observability Source: https://docs.launchtoday.dev/essentials/error-handling-observability How to ship with confidence: safe logging, correlation IDs, and Sentry/Datadog-ready patterns ## Goal In production, the fastest teams aren’t the ones who never ship bugs—they’re the ones who can **detect**, **triage**, and **fix** issues quickly. Launch aims to teach a high-quality baseline: * errors have consistent shapes/codes * logs don’t leak secrets/PII * every incident can be traced across mobile ↔ API ↔ database ## Prerequisites * API logs accessible * Error reporting configured (optional) ## Steps 1. Ensure trace IDs are returned by the API 2. Sanitize logs and error payloads 3. Add client‑side error reporting ## What to implement (recommended baseline) ### 1) Correlation IDs Every API request should have a stable identifier (`requestId` / `traceId`) that appears in: * API logs * API error responses * client error reports (Sentry breadcrumbs) This is the foundation of debugging “what happened to this user” in Datadog/Sentry. **Launch implementation:** * API responses include `X-Trace-Id` * Server logs attach the same `traceId` * Error responses include `{ traceId }` for easy copy/paste ### 2) Safe logging Rules: * never log auth cookies, tokens, idTokens, refresh tokens * sanitize request bodies and headers before logging * log structured data (JSON) so Datadog can index fields ### 3) Consistent error taxonomy Keep a small set of stable error codes and handle them explicitly: * `UNAUTHORIZED`, `FORBIDDEN` * `VALIDATION_ERROR` * `RATE_LIMITED` * `UPSTREAM_TIMEOUT`, `UPSTREAM_ERROR` * `INTERNAL_ERROR` Client UI should branch on codes, not string matching. ### 4) Mobile UX patterns * a global error boundary for unexpected crashes * friendly error messages with retry actions for network failures * treat cancellations (e.g. aborting streaming) as non-errors ## Sentry / Datadog guidance (high-level) ### Mobile Recommended: * Sentry for JS + native crash reporting * optional Datadog RUM for performance + network tracing Attach: * app version, platform, device type * user ID (if your privacy policy allows) Do **not** attach: * cookies, tokens, authorization headers ### API Recommended: * structured logs (Datadog log ingestion) * APM/tracing (Datadog APM or OpenTelemetry) * optional Sentry Node for exception + performance traces ## Production checklist * rate limit auth + expensive endpoints (like streaming) * ensure error responses don’t leak internal stack traces * ensure logs are sanitized * confirm you can correlate a mobile error → API logs using a requestId/traceId ## Incident debugging Use the step‑by‑step guide in [Incident Debugging](/essentials/incident-debugging). ## Troubleshooting * **Missing trace IDs**: verify middleware and headers * **No logs in prod**: confirm log level and sink ## Next Steps * [Incident Debugging](/essentials/incident-debugging) # Incident Debugging Source: https://docs.launchtoday.dev/essentials/incident-debugging How to trace a user issue from mobile → API → database ## Goal When a user reports a problem, you should be able to answer: * What request failed? * Why did it fail? * What data was affected? This guide uses Launch’s trace IDs to connect the dots across the app and API. ## Prerequisites * You can access API logs * You can query the database * You know the user’s email or user ID ## Steps ### 1) Capture a trace ID From the client: * In error UI, capture the `traceId` returned by the API. * If you’re testing locally, inspect the network response headers for `X-Trace-Id`. ### 2) Find the API request Search logs for the trace ID: ``` traceId= ``` Look for: * `path`, `method`, `status` * `error` message * `userId` (if logged) ### 3) Identify the failing operation Examples: * `POST /api/ai/stream` → provider/network error * `POST /trpc/stripe.createPaymentIntent` → Stripe config error * `POST /trpc/upload.requestUploadUrl` → S3 config error ### 4) Check database state Use the `userId` from logs to verify: * `users` row exists * Related records are created (e.g., `payments`, `subscriptions`, `files`) ### 5) Fix and verify * Apply the fix (env, code, config) * Reproduce the issue * Confirm logs show a success response for the same flow ## Example: payment failed → no access 1. User reports: “Payment succeeded but no access” 2. Find trace ID from client error response 3. In API logs, locate `payment_intent.succeeded` 4. Verify `payments` and `user_entitlements` for that `userId` 5. If entitlements missing, confirm Stripe product metadata and webhook logs ## Troubleshooting * **No trace ID in response**: confirm API is returning `X-Trace-Id` * **No logs for trace ID**: check log level and whether the request hit the API * **Missing `userId`**: ensure auth/session is valid for the request ## Next steps * Add Sentry breadcrumbs with `traceId` * Create a playbook for your top 3 user‑reported issues # Logging Best Practices Source: https://docs.launchtoday.dev/essentials/logging-best-practices Guidelines for reliable, privacy-safe logging in Launch ## Why this matters The goal is to make debugging in production easy without exposing sensitive information. Logging should help you answer: * What went wrong? * Where did it happen? * How often does it happen? While ensuring you never log secrets, tokens, or personal data. ## What we do in Launch Launch standardizes logging so that: * Local development logs stay in the console. * Production logs are sent to Sentry **only when enabled**. * Errors are captured with context, not raw secrets. * User‑initiated cancellations (e.g. auth cancel) are **not** logged. ## Recommended logging patterns ### 1) Use the logging helpers Prefer the utilities in `apps/mobile/lib/utils/logging.ts`: * `logInfo(message, error?)` * `logWarn(message, error?)` * `logError(message, error?)` These route to Sentry only when the Sentry feature is enabled and a DSN is set. ### 2) Log failures, not normal flows Log when something **fails**, not when it succeeds. Example: * ✅ Log: “Apple Sign‑In failed” * ❌ Don’t log: “Apple Sign‑In cancelled by user” ### 3) Keep messages short and consistent Use short, stable messages so issues group cleanly in Sentry: * `Failed to save onboarding name` * `Google Sign‑In failed` * `Stripe products API failed` ### 4) Never log secrets Don’t log: * API keys * tokens * passwords * raw request/response bodies If you need to check presence, log booleans instead: * `stripeKey: set` * `sentryDsn: missing` ### 5) Prefer context over payloads Log the **where** and **why**, not the entire payload: ✅ `Failed to update profile image`\ ❌ `Failed to update profile image: { full user object }` ## Suggested places to log High‑signal areas where production logs are most useful: * Auth failures (Apple/Google/OTP errors) * Payments and subscription errors * File upload failures * Account deletion failures * Onboarding save failures (profile setup) * AI chat failures (share, stream, and provider errors) * File upload failures (S3 flow, native uploads, multipart errors) ## How to extend safely When adding a new feature: 1. Use `logError` for failures. 2. Use `logWarn` for recoverable issues. 3. Avoid logging cancellations or expected user behaviour. 4. If you must log sensitive flows, log **presence**, never values. ## Related files * `apps/mobile/lib/utils/logging.ts` * `apps/mobile/app/auth/login.tsx` * `apps/mobile/app/onboarding/welcome.tsx` * `apps/mobile/app/auth/verify-email-otp.tsx` * `apps/mobile/app/onboarding/verify-otp.tsx` * `apps/mobile/app/onboarding/push-notifications.tsx` * `apps/mobile/app/ai-chat.tsx` * `apps/api/src/routes/ai-stream.ts` * `apps/mobile/app/payments/revenuecat.tsx` * `apps/mobile/app/payments/superwall.tsx` * `apps/mobile/app/file-uploads/s3.tsx` * `apps/mobile/app/delete-account.tsx` # Pre-Release Checklist Source: https://docs.launchtoday.dev/essentials/pre-release-checklist Finalize branding, links, and placeholders before shipping Use this checklist before you ship your app or hand the template to customers. ## Rename Launch placeholders Search for “Launch” and “launch” in these files and replace them with your app name, bundle IDs, and deep link scheme. ### Mobile app branding * `apps/mobile/app.config.ts` * `name`, `slug`, and `scheme` * iOS `bundleIdentifier` and Android `package` * `CFBundleURLSchemes` (deep link scheme) * Stripe `merchantIdentifier` * Sentry `project` and `organization` ### Stripe checkout URLs * `apps/mobile/app/payments/stripe.tsx` * `successUrl` and `cancelUrl` * Stripe `merchantIdentifier` ### File uploads (keys + storage) * `apps/mobile/app/file-uploads/s3.tsx` * Storage keys and upload queue keys (e.g., `@launch/...`) * Update to match your app namespace ### Backend docs + Docker database name * `apps/api/README.md` * App name references (“Launch API”) * Example env var values * `apps/api/docker-compose.yml` * `POSTGRES_DB`, container name, and healthcheck DB name ## Verify deep links * Ensure all `launch://` URLs are updated to your scheme * Update `MOBILE_APP_URL` in `apps/api/.env` to match ## Payments & legal links * Replace Terms/Privacy links in `apps/mobile/app/payments/stripe.tsx` * Confirm merchant identifiers and payment provider keys ## Final pass * Run a global search for “launch”, “launchhq”, and “launchtoday” * Update any remaining branding or example values # Removing Features Source: https://docs.launchtoday.dev/essentials/removing-features How to disable vs delete features (and what “world class” removal means) ## The rule of thumb Use the feature registry to **explore and learn**, then **delete features you won’t ship**. Auth/session is core infrastructure and **not** a removable feature. You can disable specific auth providers, but the session layer stays. Disabling is great for the template experience, but deletion is cleaner for production apps. ## Prerequisites * Identify which features you will ship * Access to both mobile and API code ## Steps 1. Disable the feature in the registry 2. Remove entry points and routes 3. Remove backend endpoints and env vars ## Disable vs delete ### Disable (template / exploration) Best for: * quickly trying features without setting up keys * demo builds * temporarily turning off sections during development How: * Toggle flags in `apps/mobile/features/feature-registry.tsx` * Guard routes using `FeatureGuard` (already applied for Payments/AI/File Uploads) * Hide entry points (tiles/buttons) based on `isFeatureEnabled(...)` ### Delete (production) Best for: * shipping a focused product * reducing dependencies and risk * simplifying upgrades, security review, and bundle size ## Removal checklist (use this for every feature) ### Mobile * Remove entry points (tabs, tiles, buttons, deep links) * Remove or guard all routes/screens for the feature * Remove feature providers from `FeatureProviders` * Remove feature-specific environment variables * Remove unused dependencies from `apps/mobile/package.json` ### Backend * Remove tRPC routers / REST endpoints that only exist for the feature * Remove external integrations (Stripe/S3 keys, webhook routes, etc.) * Remove database models/migrations if they’re truly feature-specific ## Removal guides (recommended) * [Auth & Session (core)](/essentials/removal-auth-session) * [Payments](/essentials/removal-payments) * [AI Chat](/essentials/removal-ai) * [File Uploads](/essentials/removal-file-uploads) ## Example: removing Payments 1. Disable in `apps/mobile/features/feature-registry.tsx` (`payments: false`) 2. Remove UI entry points: * remove the Payments tile in `apps/mobile/app/(tabs)/features/index.tsx` 3. Delete the Payments route group: * `apps/mobile/app/payments/*` 4. Remove payments providers and SDKs from `apps/mobile/lib/payments/*` and deps 5. Remove backend Stripe endpoints if not used: * `apps/api/src/routers/stripe.ts` * `apps/api/src/routes/stripe-webhooks.ts` (and related webhook wiring) 6. Remove Stripe env vars from your deployment ## Example: removing AI 1. Disable `ai: false` 2. Remove `apps/mobile/app/ai-chat.tsx` and any entry points 3. Remove API streaming route if not used: * `apps/api/src/routes/ai-stream.ts` 4. Remove OpenAI/Anthropic env vars ## Example: removing File Uploads 1. Disable `fileUploads: false` 2. Remove `apps/mobile/app/file-uploads/*` and entry points 3. Remove upload module deps from mobile if not needed 4. Remove backend S3 router if not used: * `apps/api/src/routers/upload.ts` * `apps/api/src/lib/s3.ts` 5. Remove S3 env vars ## Troubleshooting * **Feature still visible**: check feature flags and entry points * **Build errors**: remove unused deps and clean install ## Next Steps * [Feature Registry](/mobile/feature-registry) # Type safety across apps Source: https://docs.launchtoday.dev/essentials/type-safety How Launch shares API types between backend, mobile, and web ## Overview Launch uses tRPC to share API types across the API, mobile app, and web app. The API defines a single router, and both clients import the router type directly from the API package. ## Prerequisites * API package built and available * `apps/api/src/types.ts` exports `AppRouter` ## Steps 1. Define routers in `apps/api/src/router.ts` 2. Export `AppRouter` from `apps/api/src/types.ts` 3. Import `AppRouter` in mobile and web ## Single source of truth * **Router definition**: `apps/api/src/router.ts` * **Type export**: `apps/api/src/types.ts` ```ts theme={null} // apps/api/src/types.ts export type { AppRouter } from "./router"; ``` Because `apps/api/package.json` exposes `types`, both clients can import `AppRouter` from the `api` package without any manual codegen. ## Mobile client usage ```ts theme={null} // apps/mobile/lib/trpc/client.ts import type { AppRouter } from "api"; import { createTRPCReact } from "@trpc/react-query"; export const trpc = createTRPCReact(); ``` ## Web client usage ```ts theme={null} // apps/web/lib/trpc.ts import type { AppRouter } from "api"; import { createTRPCReact } from "@trpc/react-query"; export const trpc = createTRPCReact(); ``` ## Extending routers safely 1. Add a new router in `apps/api/src/routers/` 2. Register it in `apps/api/src/router.ts` 3. Use the typed hooks on mobile/web immediately Type safety is automatic as long as: * The client imports `AppRouter` from `api` * You avoid `any` or casting tRPC hooks ## Common pitfalls * **Missing router registration**: If a router isn’t added to `appRouter`, clients won’t see it. * **Casting away types**: Avoid `as any` or `as unknown` at the tRPC boundary. * **Out-of-date builds**: Restart the mobile/web dev server after adding new procedures to pick up updated types. ## Troubleshooting * **Types missing**: ensure `apps/api/package.json` points to `types` * **Type errors in mobile**: restart dev server to pick up changes ## Next Steps * [Feature Registry](/mobile/feature-registry) # Requesting Features Source: https://docs.launchtoday.dev/feature-registry/requesting-features How to suggest new features for Launch ## Requesting Features Launch focuses on foundations that most apps need. If there is a feature you think would improve the product for many teams, email us and share the details. ## What to Include * the problem you are trying to solve * who the feature helps (and how often it is needed) * links to any relevant provider or API * whether you have already built a version yourself ## Contact Email: [support@launchtodayhq.com](mailto:support@launchtodayhq.com) # API Reference Source: https://docs.launchtoday.dev/file-uploads/api-reference Complete API documentation for file upload endpoints ## Authentication All upload endpoints require authentication via tRPC context. The user must be logged in. ## Simple Upload ### requestUploadUrl Request a presigned URL for direct file upload. Name of the file being uploaded MIME type of the file (e.g., `image/jpeg`, `video/mp4`) File size in bytes **Response:** ```typescript theme={null} { uploadUrl: string; // Presigned S3 URL for PUT request fileId: string; // Database file record ID key: string; // S3 object key } ``` **Example:** ```typescript theme={null} const { uploadUrl, fileId } = await trpc.upload.requestUploadUrl.mutate({ filename: "photo.jpg", mimeType: "image/jpeg", size: 1024000, }); // Upload directly to S3 await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": "image/jpeg" }, body: fileBlob, }); ``` *** ### confirmUpload Mark a file as successfully uploaded. File record ID from `requestUploadUrl` **Response:** ```typescript theme={null} { success: boolean; file: { id: string; filename: string; url: string; } } ``` *** ## Multipart Upload For files larger than 10MB, use multipart upload for resumability. ### initiateMultipart Start a new multipart upload. Name of the file MIME type of the file Total file size in bytes Number of parts the file will be split into **Response:** ```typescript theme={null} { fileId: string; // Database file record ID uploadId: string; // S3 multipart upload ID key: string; // S3 object key } ``` *** ### getPartUrl Get presigned URL for uploading a specific part. File record ID Part number (1-indexed) **Response:** ```typescript theme={null} { uploadUrl: string; // Presigned URL for this part } ``` *** ### completePart Record that a part has been uploaded successfully. File record ID Part number that was uploaded ETag returned from S3 after uploading the part **Response:** ```typescript theme={null} { success: boolean; uploadedParts: number[]; // Array of completed part numbers } ``` *** ### completeMultipart Finalize the multipart upload after all parts are uploaded. File record ID **Response:** ```typescript theme={null} { success: boolean; file: { id: string; filename: string; url: string; } } ``` *** ### abortMultipart Cancel a multipart upload and clean up parts. File record ID **Response:** ```typescript theme={null} { success: boolean; } ``` *** ## Error Codes | Code | Description | | ----------------------- | --------------------------- | | `UNAUTHORIZED` | User not authenticated | | `BAD_REQUEST` | Invalid file type or size | | `QUOTA_EXCEEDED` | User storage quota exceeded | | `NOT_FOUND` | File record not found | | `INTERNAL_SERVER_ERROR` | S3 or database error | ## Allowed MIME Types ```typescript theme={null} const ALLOWED_MIME_TYPES = [ "image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "video/mp4", "video/quicktime", ]; ``` To add more types, modify `ALLOWED_MIME_TYPES` in `apps/api/src/routers/upload.ts`. ## Size Limits | Limit | Value | | ------------------- | ------ | | Simple upload max | 10 MB | | Multipart part size | 5 MB | | Maximum file size | 5 GB | | Default user quota | 500 MB | ## Test Checklist * `requestUploadUrl` returns a presigned URL * Upload succeeds and `confirmUpload` marks the file as uploaded * Multipart uploads complete successfully ## Troubleshooting If you see `BAD_REQUEST` or `QUOTA_EXCEEDED`, confirm allowed MIME types and storage quota configuration. ## Remove / Disable To disable uploads while you configure S3, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # Backend Setup Source: https://docs.launchtoday.dev/file-uploads/backend-setup Configure S3, environment variables, and upload API ## Prerequisites * AWS account with S3 access * IAM user with S3 permissions ## 1. Create S3 Bucket 1. Go to [AWS S3 Console](https://console.aws.amazon.com/s3) 2. Click **Create bucket** 3. Configure: * **Bucket name**: `your-app-uploads` (must be globally unique) * **Region**: Choose closest to your users * **Block Public Access**: Keep enabled (we use presigned URLs) 4. Click **Create bucket** ### CORS Configuration Add this CORS policy to your bucket: ```json theme={null} [ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"], "AllowedOrigins": ["*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3000 } ] ``` The `ExposeHeaders: ["ETag"]` is required for multipart uploads to work correctly. ## 2. Create IAM User 1. Go to [IAM Console](https://console.aws.amazon.com/iam) 2. Create a new user with **Programmatic access** 3. Attach this policy: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts" ], "Resource": [ "arn:aws:s3:::your-app-uploads", "arn:aws:s3:::your-app-uploads/*" ] } ] } ``` 4. Save the **Access Key ID** and **Secret Access Key** ## 3. Environment Variables Add these to your backend `.env`: ```bash theme={null} # AWS S3 Configuration AWS_ACCESS_KEY_ID=your_access_key_id AWS_SECRET_ACCESS_KEY=your_secret_access_key AWS_REGION=us-east-1 AWS_S3_BUCKET=your-app-uploads ``` ## 4. API Endpoints The upload router (`apps/api/src/routers/upload.ts`) provides these endpoints: ### Simple Upload | Endpoint | Description | | ------------------ | ----------------------------------- | | `requestUploadUrl` | Get presigned URL for direct upload | | `confirmUpload` | Mark upload as complete | ### Multipart Upload | Endpoint | Description | | ------------------- | ------------------------------------- | | `initiateMultipart` | Start multipart upload, get upload ID | | `getPartUrl` | Get presigned URL for a specific part | | `completePart` | Record completed part with ETag | | `completeMultipart` | Finalize multipart upload | | `abortMultipart` | Cancel and cleanup failed upload | ## 5. Database Schema The `File` model tracks uploads: ```prisma theme={null} model File { id String @id @default(cuid()) userId String filename String mimeType String size Int key String @unique url String? isUploaded Boolean @default(false) // Multipart tracking uploadId String? totalParts Int? uploadedParts Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) } ``` ## 6. Storage Quota Configure per-user storage limits in `upload.ts`: ```typescript theme={null} // Maximum storage per user (default: 500MB) const STORAGE_QUOTA = 500 * 1024 * 1024; // For testing, you can increase this const STORAGE_QUOTA = 5 * 1024 * 1024 * 1024; // 5GB ``` ## Security Considerations URLs expire after 1 hour by default. Adjust `expiresIn` in `s3.ts` if needed. Allowed MIME types are configured in `ALLOWED_MIME_TYPES`. Add/remove as needed. Consider adding rate limiting to upload endpoints in production. For user-generated content, consider AWS Lambda + ClamAV for scanning. ## Test Checklist * API starts without S3 errors * `requestUploadUrl` returns a presigned URL * Uploaded file can be confirmed in the database ## Troubleshooting If uploads fail, re-check S3 credentials and bucket CORS settings. ## Remove / Disable To disable uploads while you configure S3, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # File Uploads Source: https://docs.launchtoday.dev/file-uploads/index Secure, resumable file uploads with S3 presigned URLs ## Overview Launch includes a production-ready file upload system that supports: * **Secure uploads** via S3 presigned URLs (no credentials exposed to client) * **Resumable multipart uploads** for large files * **Native background uploads** for iOS/Android (survives app backgrounding) * **Parallel uploads** for multiple files * **Progress tracking** with persistence across navigation ## Prerequisites * S3 bucket and credentials * Backend running with S3 env vars (copy from `apps/api/example.env`) * File uploads feature enabled in `apps/mobile/features/feature-registry.tsx` ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Upload Flow │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Mobile App Backend API AWS S3 │ │ ────────── ─────────── ────── │ │ │ │ 1. Select file ──────────────► │ │ │ │ 2. Request URL ──────────────► Validate user │ │ Check quota │ │ Generate presigned URL ◄──────► │ │ ◄──────────── Return URL │ │ │ │ 3. Upload file ──────────────────────────────────────────────► │ │ │ │ 4. Confirm ──────────────────► Mark as uploaded │ │ Update user storage │ │ ◄──────────── Success │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Upload Strategies Launch automatically selects the best upload strategy based on file size: | File Size | Strategy | Description | | --------- | --------------------- | ---------------------------------------- | | \< 10 MB | **Simple Upload** | Single presigned URL PUT request | | 10-40 MB | **Multipart Upload** | Chunked upload with 5MB parts, resumable | | > 40 MB | **Native Background** | iOS URLSession / Android coroutines | ## Features Secure, time-limited URLs. No S3 credentials on client. Multipart uploads can resume after network interruption. Native iOS/Android uploads continue when app is backgrounded. Per-file progress with UI persistence across navigation. ## Steps 1. Configure S3 and backend env vars: [Backend Setup](/file-uploads/backend-setup) 2. Integrate the mobile UI: [Mobile Setup](/file-uploads/mobile-setup) 3. Enable the feature flag: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = true` ## How It Works * Mobile requests presigned URLs from the API * Uploads go directly to S3 * The API confirms and records uploads ## Quick Links Configure S3, environment variables, and API endpoints. Integrate file uploads in your React Native app. Deep dive into iOS/Android background upload module. Complete API documentation for upload endpoints. ## Test Checklist * Select a photo and upload successfully * Large file triggers multipart or native upload * Uploaded file appears in the list ## Troubleshooting Start with [Troubleshooting](/troubleshooting) and verify S3 credentials and CORS configuration. ## Remove / Disable To disable uploads while you configure S3, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = false` For production removal guidance, see [Removing Features](/essentials/removing-features). ## Next Steps * [Backend Setup](/file-uploads/backend-setup) * [Mobile Setup](/file-uploads/mobile-setup) # Mobile Setup Source: https://docs.launchtoday.dev/file-uploads/mobile-setup Integrate file uploads in your React Native app ## Overview The upload screen is located at `apps/mobile/app/file-uploads/s3.tsx` and provides: * Photo picker with recent photos * Document picker for PDFs/videos * Multi-file selection * Progress tracking * Automatic upload strategy selection ## File Pickers ### Photos Uses `expo-image-picker` and `expo-media-library`: ```typescript theme={null} import * as ImagePicker from "expo-image-picker"; import * as MediaLibrary from "expo-media-library"; // Get recent photos const { assets } = await MediaLibrary.getAssetsAsync({ first: 20, mediaType: "photo", sortBy: [MediaLibrary.SortBy.creationTime], }); // Open full picker const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images", "videos"], allowsMultipleSelection: true, quality: 0.8, videoExportPreset: ImagePicker.VideoExportPreset.HighestQuality, }); ``` ### Documents Uses `expo-document-picker`: ```typescript theme={null} import * as DocumentPicker from "expo-document-picker"; const result = await DocumentPicker.getDocumentAsync({ type: ["application/pdf", "image/*", "video/mp4", "video/quicktime"], copyToCacheDirectory: true, multiple: true, }); ``` ## Upload Strategies The app automatically selects the best strategy: ```typescript theme={null} // Thresholds const MAX_SIMPLE_FILE_SIZE = 10 * 1024 * 1024; // 10MB const NATIVE_UPLOAD_THRESHOLD = 40 * 1024 * 1024; // 40MB const MAX_MULTIPART_FILE_SIZE = 5 * 1024 * 1024 * 1024; // 5GB // Selection logic if (fileSize > NATIVE_UPLOAD_THRESHOLD) { // Use native background upload (iOS/Android) await startNativeUpload(file); } else if (fileSize > MAX_SIMPLE_FILE_SIZE) { // Use multipart upload (chunked, resumable) await uploadMultipart(file); } else { // Use simple presigned URL upload await uploadSimple(file); } ``` ## Upload Queue Files are tracked in an upload queue with individual status: ```typescript theme={null} interface QueuedUpload { id: string; file: SelectedFile; status: "pending" | "uploading" | "completed" | "error"; progress: number; error?: string; } ``` The queue: * Persists to AsyncStorage for navigation resilience * Shows per-file progress * Supports parallel uploads (for native uploads) * Shows summary alert when all complete ## Progress UI The upload button dynamically shows status: ```typescript theme={null} const getUploadButtonText = () => { if (hasActiveUploads) { return `Uploading ${completedCount + 1}/${totalFiles} (${avgProgress}%)`; } if (selectedFiles.length === 0) { return "Select files to upload"; } return `Upload ${count} files (${formatFileSize(totalSize)})`; }; ``` ## Permissions Required permissions for iOS (`Info.plist`): ```xml theme={null} NSPhotoLibraryUsageDescription We need access to your photos to upload them. NSCameraUsageDescription We need access to your camera to take photos. ``` For Android (`AndroidManifest.xml`): ```xml theme={null} ``` ## Customization ### Change Upload Thresholds Adjust the constants at the top of `s3.tsx`: ```typescript theme={null} const MAX_SIMPLE_FILE_SIZE = 10 * 1024 * 1024; // When to use multipart const NATIVE_UPLOAD_THRESHOLD = 40 * 1024 * 1024; // When to use native ``` ### Allowed File Types Modify the document picker types: ```typescript theme={null} const result = await DocumentPicker.getDocumentAsync({ type: [ "application/pdf", "image/*", "video/mp4", "video/quicktime", // Add more types here ], }); ``` ### Photo Grid Size ```typescript theme={null} const PHOTO_SIZE = 84; // Thumbnail size in pixels ``` ## Test Checklist * Select a photo and upload successfully * Large file triggers multipart or native upload * Upload progress updates in UI ## Troubleshooting If uploads fail, verify permissions and `EXPO_PUBLIC_API_URL` in `apps/mobile/.env` (from `apps/mobile/example.env`). ## Remove / Disable To disable uploads while you configure S3, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # Native Background Uploads Source: https://docs.launchtoday.dev/file-uploads/native-uploads iOS URLSession and Android coroutines for reliable large file uploads ## Overview For files larger than 40MB, Launch uses native background upload modules that: * Continue uploading when the app is backgrounded * Handle network interruptions gracefully * Provide system-level progress tracking * Work reliably for very large files (up to 5GB) ## Module Location ``` apps/mobile/modules/@launch/react-native-file-uploader/ ├── ios/ │ └── LaunchReactNativeFileUploaderModule.swift ├── android/ │ └── src/main/java/.../LaunchReactNativeFileUploaderModule.kt ├── src/ │ ├── LaunchReactNativeFileUploaderModule.ts │ └── LaunchReactNativeFileUploader.types.ts └── index.ts ``` ## iOS Implementation Uses `URLSession` with background configuration: ```swift theme={null} private func setupBackgroundSession() { let config = URLSessionConfiguration.background( withIdentifier: "com.launch.fileuploader.background" ) config.isDiscretionary = false config.sessionSendsLaunchEvents = true config.shouldUseExtendedBackgroundIdleMode = true config.timeoutIntervalForResource = 60 * 60 * 24 // 24 hours backgroundSession = URLSession( configuration: config, delegate: self, delegateQueue: OperationQueue.main ) } ``` ### Key Features * **Background transfers**: iOS manages uploads even when app is suspended * **Automatic retries**: System retries on network failure * **Progress callbacks**: Real-time progress via delegate methods ## Android Implementation Uses Kotlin Coroutines for async uploads: ```kotlin theme={null} private fun performUpload( uploadId: String, fileUri: Uri, uploadUrl: String, contentType: String ) { uploadScope.launch { // Stream file with progress tracking val connection = URL(uploadUrl).openConnection() as HttpURLConnection connection.requestMethod = "PUT" connection.setRequestProperty("Content-Type", contentType) // Upload with progress updates inputStream.copyTo(outputStream) { bytesCopied -> val percentage = (bytesCopied * 100.0 / totalBytes) emitProgress(uploadId, bytesCopied, totalBytes, percentage) } } } ``` For true background persistence on Android, consider migrating to WorkManager. The current coroutine implementation works well but may be interrupted on app kill. ## JavaScript API ### Starting an Upload ```typescript theme={null} import { startUpload, addUploadListener, cancelUpload, } from "@/modules/@launch/react-native-file-uploader"; // Start upload const result = await startUpload({ uploadId: "unique-id", fileUri: "file:///path/to/file", uploadUrl: presignedUrl, contentType: "video/mp4", }); // Listen for progress const subscription = addUploadListener("onUploadProgress", (event) => { console.log(`${event.percentage}% uploaded`); }); // Listen for completion addUploadListener("onUploadComplete", (event) => { console.log("Upload complete!"); }); // Listen for errors addUploadListener("onUploadError", (event) => { console.error("Upload failed:", event.error); }); ``` ### Available Events | Event | Payload | | ------------------- | ------------------------------------------------- | | `onUploadProgress` | `{ uploadId, bytesSent, totalBytes, percentage }` | | `onUploadComplete` | `{ uploadId, statusCode, etag }` | | `onUploadError` | `{ uploadId, error, code?, statusCode? }` | | `onUploadCancelled` | `{ uploadId }` | ### Utility Functions ```typescript theme={null} // Get status of specific upload const status = getUploadStatus(uploadId); // Returns: { uploadId, totalBytes, uploadedBytes, status, percentage } // Get all active uploads const active = getActiveUploads(); // Returns: Array of upload status objects // Cancel specific upload await cancelUpload(uploadId); // Cancel all uploads await cancelAllUploads(); ``` ## Persistence Upload state is persisted to AsyncStorage for resilience: ```typescript theme={null} interface PersistedUpload { uploadId: string; fileId: string; filename: string; totalBytes: number; startedAt: number; queueId: string; } ``` When the user navigates away and returns: 1. Saved queue is loaded from AsyncStorage 2. Native module is queried for active uploads 3. Listeners are re-attached for ongoing uploads 4. Progress UI is restored ## Limitations **App Force-Kill**: If the user force-kills the app, uploads may be lost. iOS background URLSession tasks may complete, but the app won't receive the completion callback until next launch. Server-side tracking would be needed for full resume-after-kill support. ## Debugging Enable console logs to trace upload flow: ```typescript theme={null} console.log(`Native background upload started for ${file.name}`); ``` Check native logs: * **iOS**: Xcode Console * **Android**: `adb logcat | grep LaunchReactNativeFileUploader` ## Test Checklist * Large file (>40MB) uses native upload * Progress events fire and UI updates * Upload completes after backgrounding the app ## Troubleshooting If native uploads stall, verify device background permissions and check native logs for errors. ## Remove / Disable To disable uploads while you configure S3, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.fileUploads = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # Prerequisites Source: https://docs.launchtoday.dev/getting-started/prerequisites Install the tools required to run Launch This guide covers the minimum tooling needed to run `apps/mobile` and `apps/api` locally. This repo uses `pnpm`, but you can use `pnpm`, `npm`, or `bun` if you prefer. ## Required Tools Use the repo's `.nvmrc` via nvm: ```bash theme={null} nvm use node --version ``` If nvm prompts you to install the version, run: ```bash theme={null} nvm install 24 nvm use 24 node --version ``` ```bash theme={null} npm install -g pnpm pnpm --version ``` ```bash theme={null} npm install -g @expo/cli expo --version ``` ```bash theme={null} git --version ``` Install ngrok for authentication callbacks and mobile API access: ```bash theme={null} brew install ngrok/ngrok/ngrok ``` Docker is the easiest way to run PostgreSQL locally. ## Mobile Runtime Options For day-to-day development, a local development build is the most reliable option and matches production behavior. You can run iOS and Android from the workspace using `pnpm ios` or `pnpm android`. If you prefer emulators, iOS requires Xcode and Android requires Android Studio. Expo Go is available for quick UI checks, but it does not support native modules that require custom config. If you are using a physical device, make sure it is on the same Wi‑Fi network as your computer. ## Expo Go Limitations Expo Go runs a generic client and cannot load native code added by this repo. Use a development build (and often a physical device) when you need native SDKs like: * RevenueCat * Push notifications * Other native SDKs with custom config plugins ## Store Accounts (for release) Releasing to the app stores requires paid developer accounts. Apple releases use App Store Connect with an Apple Developer account, and Android releases use Google Play Console with a Google Play developer account. ## Next Steps Continue to [Project Setup](/getting-started/setup) to fork, clone, and install dependencies. # Start the Mobile App Source: https://docs.launchtoday.dev/getting-started/run-mobile Install dependencies and run the mobile app This guide assumes your backend is running locally. ## Prerequisites * Backend running on `http://localhost:3001` * `apps/mobile/.env` available ## Steps ## 1. Copy the Example Environment File ```bash theme={null} cd apps/mobile cp example.env .env ``` The example env already includes everything needed for a first run. ## 2. Install Dependencies ```bash theme={null} cd ../.. pnpm install cd apps/mobile ``` If you already ran `pnpm install` during backend setup, you can skip this step. ## 3. Set App Identity Before you prebuild, update the app identity in `apps/mobile/app.config.ts`: * `name` (this is the display name users see after install) * `slug` (the internal Expo identifier, usually kebab-case) * `ios.bundleIdentifier` and `android.package` (your app IDs) Example: * `name`: `Finance App` * `slug`: `finance-app` * `ios.bundleIdentifier`: `com.company.financeapp` * `android.package`: `com.company.financeapp` ## 4. Prebuild Before prebuild, set your bundle ID in `apps/mobile/app.config.ts` (the file ships with a placeholder). This should match your Apple/Google identifiers before you generate native projects. ```bash theme={null} pnpm prebuild ``` You will be prompted to set your bundle ID during prebuild. If you see a warning about uncommitted changes, it is safe to continue for this step. ## 5. Run the App ```bash theme={null} pnpm ios pnpm android ``` Choose either iOS or Android. You can run on a simulator or a physical device. For iOS, a physical device on the same Wi‑Fi network is supported. When you run these commands, you'll be prompted to pick a simulator or device. If your device is on the same Wi‑Fi, you should see it in the list; otherwise you'll see available simulators/emulators. If no simulators appear, set up your platform tools first: * iOS: install Xcode and the iOS Simulator * Android: install Android Studio and create an emulator Some integrations (like RevenueCat and push notifications) require a physical device and a development build. Expo Go will not support them. ## Success Check You should see the Launch onboarding flow on first run. During build, a good sign is seeing logs like: ``` › Using --device › Signing and building iOS app with: Apple Development: () › Planning build ``` If you're on iOS, you should see: App setup complete (iOS) If you're on Android, you should see: App setup complete (Android) ## Troubleshooting * **Auth or notifications warnings**: safe to ignore during initial setup if those plugins are commented out in `apps/mobile/app.config.ts`. Enable them later when you reach the auth or push guides. * **Bundle ID not available**: pick a unique `ios.bundleIdentifier` and `android.package` (e.g. `com.yourcompany.myapp`) and rerun prebuild. * **Android package name invalid**: use only letters/numbers with dots separating segments, and make sure each segment starts with a letter (e.g. `com.company.financeapp`). ## Next Steps * [Project Structure](/project-structure) * [Authentication Setup](/authentication/backend-setup) # Project Setup Source: https://docs.launchtoday.dev/getting-started/setup Fork, clone, and install dependencies Use this guide once you have the required tools installed. ## Steps Fork the repository on GitHub so you can open pull requests later. ```bash theme={null} git clone https://github.com//launch.git cd launch ``` `pnpm` is the default, but `npm` or `bun` work too. ```bash theme={null} pnpm install ``` # Welcome Source: https://docs.launchtoday.dev/index A production-ready React Native + Expo starter ## Build and Ship Faster Launch is a production-ready React Native + Expo starter that gives you a real app baseline with the systems you typically need before launch. It is designed to get you building quickly without locking you into a rigid product shape. One codebase covers **iOS**, **Android**, **iPad**, and **Android tablets**. Use it for a consumer app, SaaS companion, or internal tool—then customize the product layer to match your needs. **Start here:** [Project Structure](/project-structure) explains how the monorepo fits together, then jump to [Quickstart](/quickstart) to run the API. *** ## Foundations Included Launch isn't opinionated about the product. It provides the foundations so you can move faster: ### Authentication **Better Auth** provides the authentication layer for the app and API, with Apple Sign-In, Google Sign-In, and email OTP. Sessions persist securely via Expo SecureStore, and protected routes keep auth‑gated screens consistent. → [Set up authentication](/authentication/backend-setup) · [Better Auth](https://www.better-auth.com/) ### Payments and Subscriptions Integrate **Stripe** for one‑time payments or subscriptions, **RevenueCat** for subscription management and entitlements, and **Superwall** for paywall presentation and experiments. The backend handles webhooks, receipt validation, and subscription status. → [Explore payment options](/payments) · [Stripe](https://stripe.com/gb) · [RevenueCat](https://www.revenuecat.com/) · [Superwall](https://superwall.com/) ### File Uploads Upload photos, documents, or large media using **AWS S3** presigned URLs. Smaller files upload directly to S3; large files use multipart uploads; long‑running transfers run in the background with native upload support. → [Learn about file uploads](/file-uploads) · [AWS S3](https://aws.amazon.com/s3/) ### AI Features Add AI chat with streaming responses via a custom backend. The API proxies OpenAI or Anthropic requests (with auth), streams responses over SSE, and keeps models and prompt handling centralized. → [Add AI features](/ai-features/overview) ### Push Notifications Register devices, manage tokens, and send notifications across iOS and Android. FCM and APNs are wired in, with server routes for sending pushes and running simple campaigns (by cohort, platform, or user list). → [Set up push notifications](/push-notifications) · [Firebase](https://firebase.google.com/) · [Apple APNs](https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns) ### Onboarding and Theming Ship a guided onboarding flow with name capture, optional phone OTP, and push permission setup. Theming includes light/dark preference storage plus a HeroUI theme layer you can swap per app style. → [See onboarding flow](/mobile/onboarding) *** ## How Features Plug In Launch uses a **Feature Registry** to keep optional modules (payments, AI, uploads, Sentry, OneSignal) in one place. Each feature declares whether it is enabled, which providers it mounts, which routes it adds, and what setup or env vars it requires. The app composes enabled providers in order, so you can add or remove a feature without hunting through the codebase. → [Feature registry guide](/mobile/feature-registry) *** ## Stack and Integrations Launch is fully TypeScript with end‑to‑end types from Prisma to the mobile UI. **Stack** * **Mobile:** React Native + Expo * **Navigation:** Expo Router * **UI:** HeroUI + NativeWind ([HeroUI Native](https://v3.heroui.com/docs/native/getting-started)) * **API:** tRPC * **Database:** Prisma + PostgreSQL * **Auth:** Better Auth * **Monorepo:** Turborepo **Integrations** * **AWS S3** → [File uploads](/file-uploads) * **Stripe** → [Stripe setup](/payments/stripe-setup) * **RevenueCat** → [RevenueCat setup](/payments/revenuecat-setup) * **Superwall** → [Superwall setup](/payments/superwall-setup) * **OpenAI / Anthropic** → [AI features](/ai-features/overview) * **Expo EAS** → [EAS deployment](/deployment/mobile-eas) * **Firebase (FCM)** → [Push notifications](/push-notifications) * **APNs** → [Push notifications](/push-notifications) Get your development environment running Understand how everything fits together *** **Questions?** Check [Troubleshooting](/troubleshooting) for common issues, or explore the docs to learn how each feature works. # Why Source: https://docs.launchtoday.dev/intro/why-launch Why Launch exists and who it is for ## Why We Built Launch Launch exists to remove the cost and time of rebuilding foundational features like authentication, payments, security, and infrastructure. Every app needs the same base, but most teams rebuild it from scratch—burning engineering hours and, in the AI era, a lot of tokens. Launch ships those foundations as a clean, modern baseline so you can focus on product work sooner. ## Who It Is For * teams building a new React Native app and wanting a strong baseline * founders shipping a first version without hiring an infrastructure team * agencies delivering custom apps faster with a reusable foundation * teams that want a real, editable codebase instead of a black‑box template ## What This Means For You You get a working, production‑oriented app with authentication, payments, uploads, push notifications, AI integration patterns, and a backend that is already wired. For businesses, it is more cost‑effective to start from a professional, unopinionated template and apply your own skin and product design than to rebuild foundations again. You can keep what you need, remove what you do not, and scale the product layer without fighting the scaffolding. # Adding New Screens Source: https://docs.launchtoday.dev/mobile/adding-screens Step-by-step guide for adding new screens to the mobile app with proper navigation # Adding New Screens to the Mobile App This guide walks you through adding new screens to the Launch mobile app, including proper navigation setup, route registration, and navigation guards. ## Overview The Launch mobile app uses a **nested navigation structure** with NativeTabs inside an AppStack: ``` AppStack (main navigation) ├── Authentication routes (/auth/*) ├── Onboarding routes (/onboarding/*) ├── (tabs) ← NativeTabs navigator │ ├── index (Home) │ ├── explore (Features) │ └── profile (Profile) └── Modal/Feature screens (/payments, etc.) ``` ## Prerequisites * Expo Router basics * App running locally ## Steps ## Step-by-Step Process ### 1. Create the Screen Component Create your screen file in the `app/` directory: **File**: `apps/mobile/app/your-screen.tsx` ```tsx theme={null} import { View } from "react-native"; import { useTheme } from "heroui-native"; import { AppText } from "@/components/app-text"; import { ScreenContainer } from "@/components/screen-container"; export default function YourScreen() { const { colors } = useTheme(); return ( Your Screen Title Screen description or subtitle. {/* Your screen content here */} ); } ``` ### 2. Customize Screen Options (Optional) Expo Router uses file-based routing, so your screen is registered automatically. If you want custom header options, add them to the layout: **File**: `apps/mobile/app/_layout.tsx` or `apps/mobile/app/(tabs)/_layout.tsx` ```tsx theme={null} ``` ### 3. Adjust Redirect Rules (if needed) If the screen should be reachable during onboarding or auth flows, update the redirect logic in `apps/mobile/lib/hooks/useAppNavigation.ts`. ### 4. Add Navigation from Other Screens To navigate to your screen from within tabs (like Explore): **From within a tab screen**: ```tsx theme={null} import { useNavigation } from "@react-navigation/native"; const navigation = useNavigation(); const handleNavigate = () => { const parentNav = navigation.getParent(); const grandParentNav = parentNav?.getParent(); grandParentNav?.navigate("your-screen" as never); }; ``` **Using FeatureCard component**: ```tsx theme={null} ``` ## Navigation Patterns ### From Tabs to AppStack Routes When navigating from inside tabs to AppStack routes, you need to access the grandparent navigator: ```tsx theme={null} // Inside a tab screen (explore, profile, etc.) const parentNav = navigation.getParent(); // Gets NativeTabs const grandParentNav = parentNav?.getParent(); // Gets AppStack grandParentNav?.navigate("your-screen" as never); ``` ### Direct AppStack Navigation For routes at the same level in AppStack: ```tsx theme={null} // Inside an AppStack screen navigation.navigate("your-screen" as never); ``` ### Back Navigation Back navigation works automatically with the configured `headerBackTitle`: * **iOS**: Shows "\< Features" or custom back title * **Android**: Shows standard back arrow * **Gesture**: Swipe from edge works automatically ## Screen Types and Presentations ### Card Presentation (Recommended) ```tsx theme={null} presentation: "card"; // Slides in from right, standard behavior ``` ### Modal Presentation ```tsx theme={null} presentation: "modal"; // Slides up from bottom, modal-style ``` ### Fullscreen Presentation ```tsx theme={null} presentation: "fullScreenModal"; // Full screen overlay ``` ## Navigation Guards Navigation guards live in `apps/mobile/lib/hooks/useAppNavigation.ts` and handle: * Redirecting unauthenticated users to auth * Redirecting users with incomplete onboarding * Sending fully onboarded users to the main tabs ## Common Issues and Solutions ### Issue: Navigation Not Working from Tabs **Problem**: Calling `navigation.navigate()` from inside tabs doesn't work. **Solution**: Use grandparent navigation: ```tsx theme={null} const parentNav = navigation.getParent(); const grandParentNav = parentNav?.getParent(); grandParentNav?.navigate("your-screen" as never); ``` ### Issue: Automatic Redirect Away from Screen **Problem**: User gets redirected back to onboarding or tabs. **Solution**: Update redirect logic in `useAppNavigation.ts` so the new route is reachable for your target user state. ### Issue: Wrong Back Button Text **Problem**: Back button shows "(tabs)" or route name. **Solution**: Set custom back title: ```tsx theme={null} headerBackTitle: "Features"; // Shows "< Features" ``` ## Best Practices ### 1. Consistent Screen Structure * Use `ScreenContainer` for layout consistency * Follow the established padding/margin patterns * Use theme colors via `useTheme()` ### 2. Navigation Naming * Use kebab-case for route names: `"feature-name"` * Keep route names descriptive but concise * Match file names to route names when possible ### 3. Header Configuration * Always set meaningful `title` * Use appropriate `presentation` for UX * Customize `headerBackTitle` for better navigation context ### 4. Navigation Guards * Update redirect rules in `useAppNavigation.ts` when adding new flows * Consider authentication requirements * Test navigation flows thoroughly ## Example: Complete Feature Screen Here's a complete example following all best practices: **File**: `apps/mobile/app/ai-chat.tsx` ```tsx theme={null} import { View } from "react-native"; import { useTheme } from "heroui-native"; import { AppText } from "@/components/app-text"; import { ScreenContainer } from "@/components/screen-container"; export default function AIChatScreen() { const { colors } = useTheme(); return ( AI Chat Configure AI providers for intelligent conversations. {/* Provider selection and setup content */} ); } ``` **AppStack Registration**: ```tsx theme={null} ``` **Navigation from FeatureCard**: ```tsx theme={null} { const parentNav = navigation.getParent(); const grandParentNav = parentNav?.getParent(); grandParentNav?.navigate("ai-chat" as never); }} /> ``` This pattern provides a consistent, maintainable approach to adding new feature screens to the Launch mobile app. ## Troubleshooting * **Screen not accessible**: check route group and auth/onboarding guards * **Header not updating**: update the correct layout file ## Next Steps * [File Structure](/mobile/file-structure) * [Feature Registry](/mobile/feature-registry) # Mobile Authentication Source: https://docs.launchtoday.dev/mobile/authentication Complete guide to authentication implementation in the mobile app ## Overview The Launch mobile app uses [Better Auth](https://better-auth.com) with Expo integration for a secure, seamless authentication experience. This guide explains how authentication works in the mobile client and how it connects to the API. ## Prerequisites * Backend running with auth routes enabled * `EXPO_PUBLIC_API_URL` set in `apps/mobile/.env` (from `apps/mobile/example.env`) ## Steps 1. Configure auth providers in the API: [Backend Authentication Setup](/authentication/backend-setup) 2. Verify mobile auth screens in `apps/mobile/app/auth` 3. Test sign‑in on device or simulator ## Architecture ### Authentication Flow ```mermaid theme={null} graph LR A[User Opens App] --> B{Session Check} B -->|No Session| C[Landing Page] B -->|Valid Session| D[Protected App] C --> E[Login Screen] E --> F[Social/Email Auth] F --> G[Better Auth API] G --> H[Session Created] H --> D ``` ### Key Components **Location**: `apps/mobile/lib/auth/client.ts`. Central authentication client that handles Better Auth requests, secure storage, and device headers. **Location**: `app/_layout.tsx` and `lib/hooks/useAppNavigation.ts`. Navigation guards redirect users based on session and onboarding state. **Location**: `apps/mobile/lib/utils/device-info.ts`. Captures device information for security and analytics purposes. ## Implementation Details ### 1. Auth Client Setup The authentication client is configured in `lib/auth/client.ts`: ```typescript theme={null} export const authClient = createAuthClient({ // IMPORTANT: For mobile OAuth (Google), this must be reachable from your device. // Use your ngrok HTTPS URL during development (recommended). baseURL: `${process.env.EXPO_PUBLIC_API_URL}/api/auth`, plugins: [ expoClient({ scheme: "launch", // Deep link scheme storagePrefix: "launch", // SecureStore prefix storage: SecureStore, // Native secure storage }), ], fetchOptions: { customFetchImpl: async (url, init) => { const deviceHeaders = getDeviceHeaders(); // Automatically inject device info in all requests return fetch(url, { ...init, headers: { ...init?.headers, ...deviceHeaders }, credentials: "omit", }); }, }, }); ``` **Key Features:** * **Secure Storage**: Uses Expo SecureStore for session persistence * **Device Headers**: Automatically includes device information in all requests * **Deep Linking**: Handles OAuth callbacks via `launch://` scheme * **Type Safety**: Full TypeScript support with session hooks **Google OAuth (Android) requires a single origin**. If your API is exposed via ngrok (e.g. `https://xxxx.ngrok-free.app`), then: * `BETTER_AUTH_URL` (API) must use that same origin * `EXPO_PUBLIC_API_URL` (mobile) must also use that same origin If mobile starts auth at `http://192.168.x.x:3001` but the callback returns to `https://xxxx.ngrok-free.app`, you will get a **state mismatch** error. ### 2. Route Protection Authentication state determines which screens users can access. The current implementation handles redirects in `useAppNavigation.ts` and keeps the route stack defined in `app/_layout.tsx`. **Benefits:** * **Centralized**: Redirect rules live in one hook * **Automatic**: Users are routed based on auth and onboarding state * **Type Safe**: Expo Router provides full TypeScript support ### 3. Session Management Sessions are managed by a lightweight provider that calls Better Auth to fetch the current session on app start and after auth events: * **Persistence**: Sessions survive app restarts via SecureStore * **Manual refresh**: `SessionProvider` calls `authClient.getSession()` on mount and exposes a `refetch()` helper used after sign-in/sign-out * **Navigation**: `useAppNavigation` derives the auth/onboarding target route ### 4. Device Information Tracking Every authentication request includes device metadata: ```typescript theme={null} export const getDeviceHeaders = (): Record => { const deviceInfo = getDeviceInfo(); return { "X-Device-Name": deviceInfo.deviceName || "unknown", "X-Device-Model": deviceInfo.deviceModel || "unknown", "X-OS-Version": deviceInfo.osVersion || "unknown", "X-App-Version": deviceInfo.appVersion || "unknown", "X-App-Platform": deviceInfo.platform, "X-Device-Type": deviceInfo.deviceType, }; }; ``` **Use Cases:** * **Security**: Detect suspicious login patterns * **Analytics**: Understand user device distribution * **Support**: Debug issues specific to device types * **Features**: Enable/disable features based on device capabilities ## Authentication Providers ### Apple Sign-In Configured for iOS devices with automatic availability detection: ```typescript theme={null} // Check if Apple Sign-In is available const isAvailable = await AppleAuthentication.isAvailableAsync(); // Handle Apple authentication await authClient.signIn.social({ provider: "apple", idToken: { token: credential.identityToken, nonce, }, callbackURL: "/", }); ``` **Features:** * **Native UI**: Uses Apple's native sign-in button * **Secure**: Implements proper nonce generation for security * **Graceful Fallback**: Shows standard button when unavailable ### Google Sign-In Implemented via Better Auth's Expo client. The login screen calls `authClient.signIn.social({ provider: "google" })`, which opens the OAuth flow in a browser and returns to the app via the `launch://` scheme. ### Email Sign-In (OTP) Email OTP sign-in is wired end-to-end. The flow uses Better Auth's email OTP plugin and a dedicated verification screen: * `apps/mobile/app/auth/email-signin.tsx` (send code) * `apps/mobile/app/auth/verify-email-otp.tsx` (enter code) * `docs/authentication/email-signin.mdx` (setup details) ## Platform-specific Behavior ### iOS * **Apple Sign-In** uses the native `expo-apple-authentication` flow to obtain an Apple identity token and then calls Better Auth `signIn.social({ provider: "apple", idToken })`. * **Google Sign-In** uses Better Auth's Expo client for browser-based OAuth and deep links back to the app via the `launch://` scheme. ### Android * **Google Sign-In** uses the same Better Auth Expo client flow as iOS (browser OAuth + deep link). Ensure your auth flow starts and ends on the same origin to avoid state mismatch errors. * **Apple Sign-In** is not available on Android. ## Security Notes Sessions are stored in SecureStore, OAuth redirects use the `launch://` scheme, and device metadata is attached to auth requests for tracing and support. ## Next Steps * [Apple Sign-In Setup](/authentication/apple-signin) * [Google Sign-In Setup](/authentication/google-signin) * [Email Sign-In with OTP](/authentication/email-signin) * **CORS Protection**: Configured trusted origins prevent unauthorized access * **Request Signing**: Device headers provide request authenticity * **Rate Limiting**: Backend prevents brute force attacks * **Error Logging**: Comprehensive error tracking for security monitoring ## Usage Examples ### Checking Authentication State ```typescript theme={null} import { authClient } from "@/lib/auth-client"; function MyComponent() { const { data: session, isPending } = authClient.useSession(); if (isPending) return ; if (session) { return ; } return ; } ``` ### Manual Sign Out ```typescript theme={null} const handleSignOut = async () => { try { await authClient.signOut(); // User automatically redirected to public routes } catch (error) { console.error("Sign out error:", error); } }; ``` ### Accessing User Data ```typescript theme={null} const { data: session } = authClient.useSession(); if (session?.user) { const { name, email, image } = session.user; // Use user data in your components } ``` ## Troubleshooting ### Common Issues **Cause**: SecureStore permissions or configuration issue **Solution**: 1. Check that `expo-secure-store` is properly installed 2. Verify the storage prefix matches your app configuration 3. Test on a physical device (simulator may have limitations) **Cause**: Missing Apple Developer configuration **Solution**: 1. Ensure `usesAppleSignIn: true` in app.config.ts 2. Add Apple Sign-In capability in Xcode 3. Configure Apple App ID with Sign-In capability 4. Set up proper environment variables (see Environment Setup) **Cause**: Component not subscribed to auth state changes **Solution**: 1. Use `authClient.useSession()` hook in your components 2. Ensure components are wrapped in proper providers 3. Check that React Query is configured correctly ### Debug Mode Enable detailed auth logging by setting: ```typescript theme={null} // In development if (__DEV__) { console.log("Auth Debug Mode Enabled"); // Additional logging will appear in console } ``` ## Environment Variables The following environment variables are required in your API backend: ```bash theme={null} # Better Auth Configuration BETTER_AUTH_SECRET=your-secret-key BETTER_AUTH_URL=http://localhost:3001 # Apple Sign-In (iOS) APPLE_CLIENT_ID=your.app.bundle.id APPLE_TEAM_ID=YOUR_TEAM_ID APPLE_KEY_ID=YOUR_KEY_ID APPLE_PRIVATE_KEY_BASE64=base64_encoded_private_key # Database DATABASE_URL=postgresql://user:password@localhost:5432/launch ``` ## Next Steps * [Apple Sign-In](/authentication/apple-signin) * [Google Sign-In](/authentication/google-signin) See the [Environment Setup Guide](/mobile/environment-setup) for complete configuration details. ## Next Steps * [Environment Setup](/mobile/environment-setup) - Configure your development environment * [File Structure](/mobile/file-structure) - Understand the mobile app organization # Component Development Source: https://docs.launchtoday.dev/mobile/components Guide to creating and using reusable components in the mobile app ## Component Architecture The Launch mobile app uses a well-organized component system that promotes reusability, consistency, and maintainability. ## Prerequisites * Familiarity with `apps/mobile/components` * `AppText` and base components available ## Steps ## Common Components Components live in a flat `components/` folder. Here are a few real examples from the codebase: ```typescript AuthButton theme={null} import { AuthButton } from "@/components/auth-button"; ``` ```typescript AppText theme={null} import { AppText } from "@/components/app-text"; Hello World ``` ```typescript ScreenContainer theme={null} import { ScreenContainer } from "@/components/screen-container"; export default function MyScreen() { return ( {/* Your screen content */} ); } ``` ```typescript ModelSelect theme={null} import { ModelSelect } from "@/components/model-select"; ``` ## Creating New Components ### Component Template ```typescript theme={null} // components/ui/my-component.tsx import { View } from "react-native"; import { AppText } from "./app-text"; import { getCopy } from "@/config/launch.config"; interface MyComponentProps { title: string; variant?: "primary" | "secondary"; onPress?: () => void; className?: string; } export function MyComponent({ title, variant = "primary", onPress, className = "", }: MyComponentProps) { const variantStyles = { primary: "bg-blue-500 text-white", secondary: "bg-gray-200 text-gray-800", }; return ( {title} ); } ``` ## Troubleshooting * **Styles not applied**: verify NativeWind classes and theme tokens * **Component not found**: check exports and import paths ## Next Steps * [File Structure](/mobile/file-structure) * [Adding Screens](/mobile/adding-screens) ### Import Pattern Components are imported directly from their files: ```typescript theme={null} import { AppText } from "@/components/app-text"; import { ScreenContainer } from "@/components/screen-container"; ``` ### TypeScript Best Practices ```typescript theme={null} // Define clear interfaces interface ButtonProps { children: React.ReactNode; variant: "primary" | "secondary" | "danger"; size?: "sm" | "md" | "lg"; disabled?: boolean; onPress: () => void; // Required callback className?: string; // Optional styling } // Use union types for variants type AlertType = "success" | "warning" | "error" | "info"; // Export types for reuse export type { ButtonProps, AlertType }; ``` ## Styling Guidelines ### NativeWind Classes Use Tailwind CSS classes through NativeWind: ```typescript theme={null} // Good - Semantic classes Title // Avoid - Arbitrary values ``` ### Theme Integration Components should respect the theme system: ```typescript theme={null} import { useTheme } from "heroui-native"; export function ThemedComponent() { const { colors, isDark } = useTheme(); return ( Themed content ); } ``` ### Responsive Design Handle different screen sizes and form factors: ```typescript theme={null} import { useResponsive } from "@/components/useResponsive"; export function ResponsiveComponent() { const { isTablet } = useResponsive(); return ( {/* Component content */} ); } ``` ## Copy Organization Launch does not ship with a centralized copy system. Keep copy close to the component or feature module it belongs to, and extract shared strings into constants when needed. ## Testing Components ### Component Testing ```typescript theme={null} // __tests__/my-component.test.tsx import { render, fireEvent } from "@testing-library/react-native"; import { MyComponent } from "../my-component"; describe("MyComponent", () => { it("renders correctly", () => { const { getByText } = render( ); expect(getByText("Test Title")).toBeTruthy(); }); it("handles press events", () => { const mockOnPress = jest.fn(); const { getByText } = render( ); fireEvent.press(getByText("Test")); expect(mockOnPress).toHaveBeenCalled(); }); }); ``` ### Visual Testing Use Storybook or component galleries for visual testing: ```typescript theme={null} // stories/MyComponent.stories.tsx export default { title: "UI/MyComponent", component: MyComponent, }; export const Primary = { args: { title: "Primary Button", variant: "primary", }, }; export const Secondary = { args: { title: "Secondary Button", variant: "secondary", }, }; ``` ## Performance Considerations ### Memoization Use React.memo for expensive components: ```typescript theme={null} import React from "react"; interface ExpensiveComponentProps { data: ComplexDataType[]; onItemPress: (id: string) => void; } export const ExpensiveComponent = React.memo( ({ data, onItemPress }) => { return ( {data.map(item => ( onItemPress(item.id)} /> ))} ); } ); ``` ### Lazy Loading Use lazy loading for heavy components: ```typescript theme={null} import { lazy, Suspense } from "react"; import { AppText } from "@/components/app-text"; const HeavyComponent = lazy(() => import("./heavy-component")); export function ParentComponent() { return ( Loading...}> ); } ``` ## Best Practices ### Component Design 1. **Single Responsibility** - Each component should have one clear purpose 2. **Composition over Inheritance** - Build complex components from simpler ones 3. **Props Interface** - Always define TypeScript interfaces for props 4. **Default Props** - Provide sensible defaults for optional props 5. **Error Boundaries** - Handle errors gracefully ### Code Organization 1. **Logical Grouping** - Place components in appropriate category folders 2. **Index Exports** - Always export from folder index files 3. **Consistent Naming** - Use PascalCase for components, camelCase for props 4. **File Naming** - Use kebab-case for file names ### Documentation 1. **Props Documentation** - Document complex props with JSDoc 2. **Usage Examples** - Provide clear usage examples 3. **Component Stories** - Create Storybook stories for visual components 4. **README Updates** - Keep component documentation current This component system ensures consistency, reusability, and maintainability across the entire mobile application. # Environment Setup Source: https://docs.launchtoday.dev/mobile/environment-setup Complete guide to setting up your development environment for the mobile app ## Overview This page covers the native toolchain and key configuration locations for the mobile app. For the step-by-step run flow, see [Start the Mobile App](/getting-started/run-mobile). ## Prerequisites * **Node.js** 24+ * **pnpm** * **Expo CLI** (`npm install -g @expo/cli`) * **Xcode** (iOS) or **Android Studio** * Optional: a physical iOS device on the same Wi‑Fi network ## Steps 1. Copy the example env: ```bash theme={null} cd apps/mobile cp example.env .env ``` 2. Install dependencies: ```bash theme={null} pnpm install ``` 3. Prebuild (prompts for bundle ID): ```bash theme={null} pnpm prebuild ``` 4. Run the app: ```bash theme={null} pnpm ios pnpm android ``` ## Key Config Locations * `apps/mobile/app.config.ts` — bundle IDs, native plugins, build settings * `apps/mobile/features/feature-registry.tsx` — feature flags and providers * `apps/mobile/lib/env.ts` — reads `EXPO_PUBLIC_*` values from `.env` ## Next Steps * [Start the Backend](/quickstart) * [Start the Mobile App](/getting-started/run-mobile) * [Authentication Setup](/authentication/backend-setup) # Feature Registry Source: https://docs.launchtoday.dev/mobile/feature-registry Enable, disable, and swap app features as building blocks ## What this is Launch treats major optional capabilities (payments, AI, uploads, etc.) as **features** that can be enabled/disabled and composed centrally. Auth/session is core infrastructure and is **not** modeled as a removable feature. You can disable specific auth providers, but the session layer stays. The goal is to make it easy for engineers to: * delete a feature without hunting through the app * swap a provider implementation (e.g., payments) without rewriting UI * understand what a feature contributes (providers, screens, env requirements) ## Prerequisites * Familiarity with `apps/mobile/features` * Optional features enabled via flags ## Steps ## The registry (source of truth) The feature registry lives in: `apps/mobile/features/feature-registry.tsx` It defines: * which features exist * whether they are enabled * the order of provider composition * optional docs links and dependencies * required env vars and permissions * removal checklists for clean deletion ## How provider composition works Some optional features require React providers (payments, uploads, etc.). Core infrastructure (like auth session, query client, theming) is mounted outside the registry. In Launch, the root layout mounts a single `FeatureProviders` component, and it wraps the app with the enabled feature providers in order. This keeps `app/_layout.tsx` clean and makes provider wiring a “one place” concern. ## Enable/disable a feature Edit `featureFlags` inside: `apps/mobile/features/feature-registry.tsx` Example: ```tsx theme={null} export const featureFlags = { payments: true, ai: true, fileUploads: true, }; ``` If you want to pin payments to a specific provider, use the object form: ```tsx theme={null} export const featureFlags = { payments: { enabled: true, provider: "stripe" }, ai: true, fileUploads: true, }; ``` If you disable a feature, you should also remove or gate its screens/components so the app can’t navigate into a broken flow. ## Production guidance: delete features you won’t ship For real production apps, the best practice is usually: * **Use the registry to explore and learn** * Then **delete features you won’t ship**, instead of leaving them disabled Why: * Disabled features still add maintenance surface (dependencies, upgrades, security review, bundle size, more routes to reason about). * “Disabled” is great for template exploration, but deletion is cleaner for long-lived products. ### Minimal removal checklist When removing a feature (example: payments), aim for these outcomes: * No entry points in UI (tiles, buttons, menus) * No reachable routes/screens * No providers mounted in `FeatureProviders` * No feature-specific env vars required * No unused dependencies left behind * No backend tables/endpoints running if they’re not needed ### When feature flags still make sense * **Build-time flags (recommended)**: dev/staging/prod builds differ by enabled features (cleanest, predictable). * **Runtime flags (advanced)**: UX experiments and gradual rollouts (never rely on runtime flags for security boundaries; enforce entitlements server-side). ## Add a new feature 1. Create the feature code (recommended home: `apps/mobile/features//`) 2. Add a new `FeatureId` 3. Add a new entry in `featureRegistry` 4. If it needs a provider, add `provider` + `order` 5. Document: * required env vars * screens/routes it adds * how to remove it cleanly ## Remove a feature World-class removal should be boring and predictable: 1. Disable it in `featureFlags` 2. Remove its routes/screens (or guard them behind the flag) 3. Remove its API wiring and env vars 4. Remove docs + navigation links ## Next step As the template evolves, each feature should declare its contract in one place: * screens/routes it contributes * required env vars and permissions * removal steps (disable vs delete) This makes “swap/remove features like building blocks” a first-class product experience. ## Troubleshooting * **Feature not visible**: verify `featureFlags` and entry points * **Provider not mounted**: check `FeatureProviders` order ## Next Steps * [Removing Features](/essentials/removing-features) # Mobile App File Structure Source: https://docs.launchtoday.dev/mobile/file-structure Understanding the organization and patterns used in the mobile app ## Overview The Launch mobile app follows a well-organized structure that makes it easy for developers to find components, understand data flow, and maintain the codebase. ## Prerequisites * Basic familiarity with Expo Router ## Steps ## Directory Structure ``` apps/mobile/ ├── app/ # Expo Router routes │ ├── _layout.tsx # Root layout/providers/navigation │ ├── (tabs)/ # Tabbed routes │ │ ├── home/ # Home tab screens │ │ ├── features/ # Feature catalog screens │ │ └── settings/ # Settings tab screens │ ├── auth/ # Auth screens │ ├── onboarding/ # Onboarding flow │ ├── payments/ # Payments screens │ ├── file-uploads/ # Uploads screens │ ├── ai-chat.tsx # AI chat screen │ ├── notifications.tsx # Notification settings │ ├── appearance.tsx # Theme and appearance │ ├── delete-account.tsx # Account deletion │ └── error-screen/ # Global error fallback ├── components/ # Reusable UI components (flat) ├── features/ # Feature modules + registry │ ├── feature-registry.tsx │ ├── ai/ │ ├── payments/ │ ├── file-uploads/ │ └── sentry/ ├── lib/ # Clients, hooks, services │ ├── auth/ # Better Auth client + session context │ ├── trpc/ # tRPC client │ ├── payments/ # Stripe/RevenueCat/Superwall │ ├── ai/ # Providers, prompts, config │ ├── api/ # API config + health │ ├── upload/ # Upload helpers │ ├── hooks/ # Shared hooks │ ├── mutations/ # Client mutations │ ├── notifications.ts # Push setup + registration │ └── google-services.json # Firebase config (Android, placeholder) ├── constants/ # Design tokens (colors/spacing/typography) ├── contexts/ # App/theme contexts ├── modules/ # Native modules (Expo Modules API) ├── themes/ # Theme definitions └── app.config.ts # Expo config (replace placeholders) ``` ## Key Patterns ### Component Organization Components live primarily in a flat `components/` folder. Grouping is done by naming and usage instead of deep subfolders (e.g., `auth-button.tsx`, `model-select.tsx`, `screen-container.tsx`). Small subfolders like `__tests__` exist for tests, but the main component surface stays flat for easy imports. ### Configuration & Feature Flags There is no `launch.config.ts` in the repo. App configuration and feature flags live in: * `app.config.ts` (Expo config, plugins, bundle identifiers) * `features/feature-registry.tsx` (feature enable/disable, providers) `app.config.ts` is required by Expo to wire native identifiers (bundle ID / package name), deep link schemes, and plugin configuration. The repo ships placeholder values so you can commit safely—replace them with your own before building. ## Screen Patterns ### Authentication Flow ```typescript theme={null} // app/_layout.tsx - Route protection ``` ### Feature Registry The feature registry composes optional modules (payments, AI, uploads, Sentry) in one place: ```tsx theme={null} // apps/mobile/features/feature-registry.tsx export const featureRegistry = [ paymentsFeature(featureFlags), aiFeature(featureFlags), fileUploadsFeature(featureFlags), sentryFeature(featureFlags), ]; ``` ### Component Composition ```typescript theme={null} // Dashboard screen using reusable components export default function HomeScreen() { const { data: session, isPending } = authClient.useSession(); if (isPending) return ; return ( Welcome {session?.user?.email} ); } ``` ## Design System Integration ### Spacing Use predefined spacing tokens instead of arbitrary values: ```typescript theme={null} // Good paddingTop: insets.top + Spacing.screen.headerSpacing, paddingBottom: insets.bottom + Spacing.md, // Avoid paddingTop: 60, paddingBottom: 16, ``` ### Colors Colors are defined in the theme system and accessed consistently: ```typescript theme={null} // Theme colors (dynamic) const { colors, isDark } = useTheme(); // Static colors from constants backgroundColor: ButtonColors.grayButton, ``` ### Typography Font families are configured in the design system: ```typescript theme={null} // Tailwind classes className = "font-sans-bold text-4xl"; // Maps to Manrope-Bold font family ``` ## Best Practices ### Component Creation 1. **Reusable components** go in `components/` (flat, with minimal subfolders) 2. **Screen-specific components** can stay in the screen file if they won't be reused 3. **Always export** from the folder's `index.ts` file 4. **Use TypeScript interfaces** for props ### State Management 1. **Authentication state** - Use `authClient.useSession()` 2. **Theme state** - Use `useTheme()` hook 3. **Form factor detection** - Use `useResponsive()` for device types 4. **Local state** - Use React's `useState` and `useEffect` ### Styling Approach 1. **NativeWind classes** for most styling 2. **Style objects** when dynamic values are needed 3. **Theme system** for colors and fonts 4. **Spacing tokens** for consistent layout ## Adding New Features ### New Screen 1. Create screen file in `app/` 2. Add route group or stack entry if needed 3. Gate behind feature registry when optional 4. Extract reusable components to `components/` ### New Component 1. Create component in `components/` 2. Write TypeScript interface for props 3. Add to the calling screen or feature module This structure ensures the codebase remains organized, scalable, and easy to understand for new developers. ## Troubleshooting * **File not routing**: confirm file name and path under `app/` * **Import issues**: use `@/` aliases consistently ## Next Steps * [Adding Screens](/mobile/adding-screens) * [Component Development](/mobile/components) # Mobile App Onboarding Source: https://docs.launchtoday.dev/mobile/onboarding Overview of the current onboarding flow and where to extend it ## Overview Launch ships with a lightweight onboarding flow that helps you capture the minimum profile data needed to personalize the app, then request permissions only when they are relevant. The default flow collects a name, optionally verifies a phone number via SMS OTP, and asks for push notification permissions. Onboarding is the moment to build trust, set expectations, and gather the data that improves first-run UX. The current flow is intentionally short so you can expand it based on your product needs. ## Prerequisites * Auth flow enabled * Onboarding routes available in `apps/mobile/app/onboarding` ## Current flow The onboarding screens live in `apps/mobile/app/onboarding/`: * `welcome.tsx` collects the user’s name * `phone.tsx` collects a phone number for SMS OTP * `verify-otp.tsx` verifies the OTP * `push-notifications.tsx` requests push permissions The flow is controlled by the feature registry and can skip SMS entirely if you do not want to set up Twilio yet. Push notifications in onboarding currently store the user’s preference, but do not enable real notifications until you complete the push notification setup later in the docs. ## Flow control Routing and step selection are handled in: * `apps/mobile/app/_layout.tsx` * `apps/mobile/lib/hooks/useAppNavigation.ts` ## Feature registry (SMS / OTP) The SMS OTP step is gated by the `oneSignal` feature flag in `apps/mobile/features/feature-registry.tsx`. When the flag is disabled, the flow skips `phone.tsx` and `verify-otp.tsx` so onboarding becomes: 1. Name 2. Push notifications This lets you ship without SMS while keeping the flow intact. ## OneSignal and SMS OTP OneSignal and SMS OTP configuration are covered in [OneSignal and SMS OTP](/mobile/one-signal). If you are not using SMS in your onboarding flow, disable the feature registry flag and skip the phone and OTP steps. ## How the backend supports onboarding The API exposes user endpoints that save onboarding data and track completion. They live in `apps/api/src/routers/user.ts` and include: * `user.saveName` * `user.savePhoneNumber` * `user.sendPhoneOtp` * `user.verifyPhoneOtp` * `user.registerDeviceToken` * `user.markOnboardingComplete` * `user.onboardingStatus` These endpoints update the user profile and onboarding flags in the database. ## Data model references Onboarding fields are stored on the `User` model in `apps/api/prisma/schema.prisma`, including name, phone number, and onboarding completion flags. Push notification device tokens are stored in the `DeviceToken` model. ## Extending onboarding To add steps: 1. Create a new file in `apps/mobile/app/onboarding/`. 2. Add routing logic in `useAppNavigation.ts` if the step should be required. 3. Link from the prior step using the router. Common additions include: * A short goal or preferences questionnaire * A paywall before the main app (if your product is subscription-first) * An account personalization step (avatar, username, interests) If you add new onboarding data, create a matching endpoint in `apps/api/src/routers/user.ts` and persist it on the `User` model in `apps/api/prisma/schema.prisma`. ## Code references * **Root Layout**: `apps/mobile/app/_layout.tsx` * **Onboarding Screens**: `apps/mobile/app/onboarding/*.tsx` * **Navigation Logic**: `apps/mobile/lib/hooks/useAppNavigation.ts` * **User Endpoints**: `apps/api/src/routers/user.ts` * **Database Schema**: `apps/api/prisma/schema.prisma` ## Best practices * Keep early steps short and low-friction. * Ask for permissions only after explaining the benefit. * Add a paywall only if it matches your product’s activation strategy. * Track each step so you can measure drop-off and improve completion. # OneSignal and SMS OTP Source: https://docs.launchtoday.dev/mobile/one-signal How OneSignal and SMS OTP are wired into onboarding ## Overview Launch uses OneSignal for push notifications and Twilio Verify for SMS OTP. During onboarding, SMS OTP is optional and can be disabled via the feature registry. When enabled, the app collects a phone number, sends an OTP via the API, and verifies it before moving on. ## Feature registry OneSignal is registered as a feature module in `apps/mobile/features/one-signal/feature.ts` and toggled via the `oneSignal` flag in `apps/mobile/features/feature-registry.tsx`. When this feature is disabled, the onboarding flow skips the phone and OTP screens and continues directly to the push notification step. ## How SMS OTP works The onboarding screens call user endpoints on the API: * `user.savePhoneNumber` * `user.sendPhoneOtp` * `user.verifyPhoneOtp` These endpoints live in `apps/api/src/routers/user.ts` and use the Twilio client in `apps/api/src/lib/twilio.ts` to send and verify OTPs. ## Required environment variables Mobile: * `ONESIGNAL_APP_ID` API (Twilio Verify): * `TWILIO_ACCOUNT_SID` * `TWILIO_AUTH_TOKEN` * `TWILIO_SERVICE_SID` Optional dev-only flag: * `SKIP_TWILIO_OTP_VERIFICATION` (useful for local testing when Twilio is not configured) ## Where it is used in the app * Onboarding screens: `apps/mobile/app/onboarding/phone.tsx`, `apps/mobile/app/onboarding/verify-otp.tsx` * Push permissions: `apps/mobile/app/onboarding/push-notifications.tsx` * Flow control: `apps/mobile/lib/hooks/useAppNavigation.ts` # Database Schema Source: https://docs.launchtoday.dev/payments/database-schema Understanding the payment system database design and relationships # Payment Database Schema The payment system uses a flexible database schema designed to support multiple billing models and payment providers while maintaining clean separation of concerns. ## Schema Design Philosophy ### Plans and Subscriptions The schema focuses on: 1. **Plans** for pricing metadata 2. **Subscriptions** for ongoing access 3. **Payments** for charge history This separation allows you to: * Change pricing without affecting feature logic * Support multiple billing models simultaneously * Switch payment providers easily ## Core Models ### Plan Model Located in `apps/api/prisma/schema.prisma` Stores subscription plans and their Stripe integration details: * Links to Stripe products and prices * Supports multiple billing intervals (monthly, yearly, one-time) * Currency and amount tracking * Active/inactive status for plan management **Key Fields**: * `stripePriceId` - Links to Stripe Price objects * `stripeProductId` - Links to Stripe Product objects * `interval` - Billing frequency (monthly, yearly, one\_time) * `amount` - Price in cents for display purposes ### Subscription Model Tracks active user subscriptions with full Stripe integration: * Links users to their active plans * Stores Stripe subscription and customer IDs * Tracks subscription status and billing periods * Handles cancellation scheduling **Key Relationships**: * Belongs to a `User` * References a `Plan` * Synced with Stripe via webhooks ### Payment Model Stores individual Stripe payment intents: * Links users to Stripe payment intents and customers * Tracks success/failure status * Records paid timestamps ## Indexing Strategy The schema includes strategic indexes for performance: ### Primary Lookups * `User.email` - Authentication and user lookup * `Payment.stripePaymentIntentId` - Payment lookup ### Stripe Integration * `Plan.stripePriceId` - Webhook processing * `Subscription.stripeSubscriptionId` - Webhook processing * `Subscription.userId` - User subscription queries ## Relationships Overview ``` User ├── subscriptions[] → Subscription └── payments[] → Payment Subscription ├── user → User └── plan → Plan Plan └── subscriptions[] → Subscription ``` ## Example Scenarios ### Payment History 1. Query `Payment` for user + recent records 2. Display status and timestamps 3. Use Stripe IDs for reconciliation if needed ### Subscription Changes 1. Stripe webhook updates `Subscription` status 2. System updates internal `Subscription` status 3. User immediately sees new access levels ## Migration Commands After updating the schema, run these commands: ```bash theme={null} cd apps/api pnpm db:migrate pnpm db:generate ``` This creates the migration files and updates the Prisma client with the new models. # Payments Source: https://docs.launchtoday.dev/payments/index Understand the payment options and where they connect # Payment Integrations Payments turn onboarding and engagement into revenue. Launch ships with integrations that cover direct billing, subscriptions, and paywall management so you can pick the model that fits your product. The payment layer is already wired into the app, but it is disabled by default. Your first step is to enable the payments feature flag, then follow the setup guide for the provider you choose. ## Prerequisites * Payments feature enabled in `apps/mobile/features/feature-registry.tsx` * Backend env vars set for your chosen provider (copy from `apps/api/example.env`) * Mobile env vars set if required (copy from `apps/mobile/example.env`) Direct payment processing with full control. Best for web + mobile apps or custom billing flows. Simplified in-app subscriptions with cross-platform support. Best for subscription-first apps. Paywall A/B testing and optimization. Best for maximizing subscription revenue. ## Which should I use? | Integration | Best For | Complexity | App Store Fees | | -------------- | -------------------------- | ---------- | -------------- | | **Stripe** | Web + Mobile, Custom flows | Medium | You handle | | **RevenueCat** | Subscription apps | Low | Handled | | **Superwall** | Optimizing conversions | Low | Handled | For most mobile apps, we recommend starting with **Stripe** for maximum flexibility, then adding **RevenueCat** or **Superwall** if you need advanced subscription management or A/B testing. ## Steps 1. Enable the payments feature flag in `apps/mobile/features/feature-registry.tsx`. 2. Choose a provider: * [Stripe](/payments/stripe) * [RevenueCat](/payments/revenuecat) * [Superwall](/payments/superwall) 3. Follow the setup guide for your provider. ## How It Works Launch wraps payment providers behind a unified interface. The provider is selected in `apps/mobile/features/feature-registry.tsx`, and the UI reads from the provider layer. Key references: * Feature flag: `apps/mobile/features/feature-registry.tsx` * Provider selection: `apps/mobile/features/feature-registry.tsx` * Payments UI: `apps/mobile/app/(tabs)/features/index.tsx` # Paddle (Coming Soon) Source: https://docs.launchtoday.dev/payments/paddle Merchant of record with global tax handling for international sales # Paddle Integration Paddle integration is coming soon. This page will be updated with setup instructions and implementation details. ## What is Paddle? Paddle is a merchant of record (MoR) platform that handles the complexity of selling software globally: * **Tax Compliance** - Automatic VAT, sales tax, and GST handling * **Global Payments** - Accept payments in 200+ countries * **Merchant of Record** - Paddle is the seller, handling all compliance * **Invoicing** - Automatic invoice generation for customers ## Why Use Paddle? Automatic tax calculation and remittance worldwide. Accept payments from 200+ countries with local methods. Paddle handles regulatory compliance as merchant of record. Built-in support for invoicing and enterprise sales. ## When to Use Consider Paddle if you: * Sell to customers globally and need tax compliance * Want someone else to handle VAT/GST/sales tax * Need proper invoicing for B2B customers * Want to avoid setting up legal entities in multiple countries ## Coming Soon * Installation guide * SDK configuration * Checkout integration * Webhook handling * Subscription management In the meantime, check out the [Stripe integration](/payments/stripe) for a fully implemented payment solution. # Paywall Best Practices Source: https://docs.launchtoday.dev/payments/paywall-best-practices Build high-converting paywalls with proven psychological principles Building an effective paywall is both an art and a science. This guide covers the psychology behind high-converting paywalls and how to apply these principles to your app. Launch paywall overview The complete paywall screen with all conversion elements ## The Psychology of Conversion Users make purchasing decisions based on emotion first, then justify with logic. Your paywall should: 1. **Create desire** — Show what they're missing 2. **Build trust** — Social proof and guarantees 3. **Remove friction** — Clear pricing, easy action 4. **Reduce risk** — Money-back guarantees, cancel anytime *** ## Key Conversion Elements ### 1. Clear Value Proposition Your title should immediately communicate what the user gains. Use action verbs like "Unlock" or "Get" to imply transformation. Keep your title under 8 words. Users scan, they don't read. *** ### 2. Price Anchoring Show the yearly price with a monthly breakdown (e.g., "$199.99/year ($16.67/mo)"). This makes the yearly option feel more affordable and encourages longer commitments. Always display a savings badge like "Save 17%" to create urgency and highlight the value of annual plans. Show the yearly plan first and pre-select it. Users are more likely to choose the first option they see. *** ### 3. Plan Selection Cards Plan selection cards Your plan options should have: * Clear visual distinction between selected and unselected states * Badge support for highlighting value (e.g., "Most Popular", "Best Value") * Haptic feedback on selection for tactile confirmation * Large, accessible touch targets *** ### 4. Benefits List Benefits list Lead with your most valuable benefit and use specific language. "Cloud sync across all your devices" is more compelling than "Sync to multiple devices." Include emotional benefits like "Priority support when you need help" — users want to feel cared for. Keep to 4-6 items maximum. Too many benefits dilute the message. *** ### 5. Social Proof Testimonial carousel Testimonials are one of the most powerful conversion tools. The Launch template includes a **TestimonialCarousel** that auto-rotates quotes in the sticky CTA area — ensuring users always see social proof near the purchase button. Use real testimonials from actual users. Fake testimonials erode trust and may violate consumer protection laws. *** ### 6. Trust Signals #### Money-Back Guarantee A guarantee like "7-day money-back guarantee" removes purchase anxiety, shows confidence in your product, and reduces perceived risk. #### Cancel Anytime For subscriptions, always show "Cancel anytime" prominently. This addresses subscription fatigue and helps users feel in control. *** ### 7. Sticky CTA Sticky CTA container Your call-to-action button should be fixed at the bottom of the screen, always visible regardless of scroll position. Users should never have to search for how to purchase. The Launch template positions testimonials, the purchase button, and trust signals together in this sticky area for maximum impact. *** ### 8. Dynamic Button Text Your subscribe button should show the exact price and plan (e.g., "Subscribe – \$199.99/year"). This eliminates surprises at the decision point and reinforces the selected option. *** ## Conversion Optimization Checklist | Element | Purpose | | ---------------------- | ---------------------------- | | Clear title | Communicate value instantly | | Price anchoring | Make yearly plans attractive | | Monthly breakdown | Reduce sticker shock | | Savings badge | Create urgency | | Benefits list | Justify the purchase | | Testimonials | Social proof | | Money-back guarantee | Reduce risk | | Cancel anytime | Reduce commitment fear | | Sticky CTA | Always accessible | | Dynamic button text | Clarity at decision point | | Apple Pay / Google Pay | Reduce checkout friction | *** ## A/B Testing Ideas Once you have users, consider testing: 1. **Pricing** — Different price points 2. **Plan order** — Yearly first vs monthly first 3. **Title copy** — Different value propositions 4. **Testimonial content** — Which quotes resonate most 5. **CTA text** — "Subscribe" vs "Get Pro" vs "Upgrade Now" 6. **Guarantee length** — 7 days vs 14 days vs 30 days *** ## Common Mistakes to Avoid 1. **Too many options** — Stick to 3 plans maximum 2. **Hidden pricing** — Always show the price upfront 3. **Fake urgency** — "Only 2 left!" erodes trust quickly 4. **Wall of text** — Users scan, keep it minimal 5. **No social proof** — Testimonials significantly boost conversion 6. **Difficult dismissal** — Let users close the paywall easily *** ## What's Included in Launch Paywall in dark and light mode The Launch paywall template implements all of these best practices out of the box: * **SelectCard component** — Beautiful, accessible plan selection * **TestimonialCarousel** — Auto-rotating social proof * **Sticky CTA container** — Always-visible purchase area * **Dynamic pricing display** — Shows monthly breakdown for yearly plans * **Trust signals** — Guarantee and cancel anytime messaging * **Apple Pay / Google Pay** — One-tap purchasing All content is centralized in a single `PAYWALL_CONTENT` object for easy customization. *** ## Next Steps Configure your products and pricing Enable one-tap purchasing ## Apply in Launch The default paywall lives in `apps/mobile/app/payments/stripe.tsx`. Customize copy and options in the `PAYWALL_CONTENT` object. ## Remove / Disable To disable payments while you configure providers, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # RevenueCat Overview Source: https://docs.launchtoday.dev/payments/revenuecat In-app subscriptions made easy with cross-platform support RevenueCat is a subscription management platform that simplifies in-app purchases across iOS, Android, and web. ## What's Included Launch includes a ready-to-use RevenueCat integration: * **RevenueCat Provider** - Context for subscription state * **Native Paywall** - Uses RevenueCat's visual paywall builder * **Entitlement Checking** - `useRevenueCat()` hook * **Restore Purchases** - Built-in restore functionality ## Why RevenueCat? Design paywalls in RevenueCat's dashboard — no code changes needed. Sync subscriptions between iOS, Android, and web. Built-in dashboards for revenue and subscriber metrics. Test different paywalls and pricing without app updates. ## How It Works ``` App Store Connect RevenueCat Your App ───────────────── ────────────── ───────── Products & ──▶ Syncs products ──▶ Fetches offerings Subscriptions Manages entitlements Shows paywall Validates receipts Checks access ``` 1. **App Store Connect** - Create your subscription products 2. **RevenueCat** - Import products, create offerings, design paywalls 3. **Your App** - Display paywall, check subscription status ## Usage ### Check Subscription Status ```tsx theme={null} import { useRevenueCat } from "@/lib/payments/revenuecat"; function MyComponent() { const { isSubscribed, subscriptionStatus } = useRevenueCat(); if (isSubscribed) { return ; } return ; } ``` ### Navigate to Paywall ```tsx theme={null} import { useRouter } from "expo-router"; function UpgradeButton() { const router = useRouter(); return ( ); } ``` ## File Structure ``` apps/mobile/lib/payments/ ├── config.ts # API keys and env helpers ├── revenuecat/ │ ├── provider.tsx # RevenueCatProvider context │ └── index.ts # Exports └── types.ts # Shared types apps/mobile/app/payments/ └── revenuecat.tsx # Paywall screen (uses RevenueCat UI) ``` ## Test Checklist * Offerings load in the paywall * Sandbox purchase succeeds * Entitlement state updates in-app ## Troubleshooting See [RevenueCat Troubleshooting](/payments/revenuecat-troubleshooting). ## Remove / Disable To disable RevenueCat while you configure products, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). ## Next Steps Configure App Store Connect and RevenueCat Common errors and solutions # RevenueCat Setup Source: https://docs.launchtoday.dev/payments/revenuecat-setup Step-by-step guide to configure RevenueCat with App Store Connect This guide walks you through setting up RevenueCat from scratch. **First run tip:** If you haven’t configured products yet, disable the payments feature to avoid “offerings empty” errors: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false`. Re-enable after setup is complete. ## Prerequisites * Apple Developer account (\$99/year) * App created in App Store Connect * RevenueCat account (free tier available) *** ## Step 1: App Store Connect ### 1.1 Create Subscription Products 1. Go to [App Store Connect](https://appstoreconnect.apple.com) 2. Select your app → **Monetization** → **Subscriptions** 3. Create a **Subscription Group** (e.g., "Premium") 4. Add your subscription products ### 1.2 Product Configuration For each product, configure: | Field | Example | | -------------- | --------------------------- | | Product ID | `mobile_launch_monthly_sub` | | Reference Name | "Monthly Subscription" | | Duration | 1 Month | | Price | Select a price tier | ### 1.3 Add Localization **Required!** Each product needs at least one localization: 1. Click on your product 2. Go to **Localizations** → **Add** 3. Add **Display Name** and **Description** Products without localization won't load in your app! ### 1.4 Accept Paid Apps Agreement 1. Go to **Agreements, Tax, and Banking** 2. Accept the **Paid Apps Agreement** 3. Complete all required tax and banking information Products won't load until this agreement is active. *** ## Step 2: RevenueCat Dashboard ### 2.1 Create Project 1. Sign up at [RevenueCat](https://app.revenuecat.com) 2. Create a new project 3. Add your iOS app with the correct bundle ID ### 2.2 Connect App Store 1. Go to **Apps & providers** → Your iOS app 2. Add your **App Store Connect Shared Secret**: * In App Store Connect: Your App → General → App-Specific Shared Secret * Click "Generate" if you don't have one * Copy and paste into RevenueCat ### 2.3 Import Products 1. Go to **Product Catalog** → **Products** 2. Click **+ New** under your App Store app 3. Enter the Product ID exactly as in App Store Connect 4. Products should sync automatically ### 2.4 Create Entitlements Entitlements define what features users get access to: 1. Go to **Product Catalog** → **Entitlements** 2. Click **+ New** 3. Create an entitlement (e.g., `pro` or `premium`) 4. Attach your products to this entitlement ### 2.5 Create Offerings Offerings group products for display in your paywall: 1. Go to **Product Catalog** → **Offerings** 2. Click **+ New** 3. Name it (e.g., `default`) 4. Add packages: * `$rc_monthly` → your monthly product * `$rc_annual` → your yearly product 5. Click **Make Current** to set as default ### 2.6 Get API Keys 1. Go to **API Keys** in sidebar 2. Copy your **Public API Key** * iOS: starts with `appl_` * Android: starts with `goog_` *** ## Step 3: Configure Your App ### 3.1 Add API Keys Update API keys in `lib/payments/config.ts`: ```typescript theme={null} export const REVENUECAT_CONFIG = { iosApiKey: "appl_your_api_key_here", androidApiKey: "goog_your_api_key_here", }; ``` ### 3.2 Set Payment Provider Update the feature registry: ```typescript theme={null} export const featureFlags = { payments: { enabled: true, provider: "revenuecat" }, // ... }; ``` *** ## Step 4: Create a Paywall RevenueCat lets you design paywalls without code: 1. Go to **Paywalls** in RevenueCat dashboard 2. Click **+ New Paywall** 3. Use the visual builder to design your paywall 4. Add your products, benefits, and styling 5. Assign the paywall to your offering The `` component will automatically display this paywall. *** ## Step 5: Testing ### Create Sandbox Tester 1. App Store Connect → **Users and Access** → **Sandbox** 2. Click **+** to add a new tester 3. Use a unique email (can be fake, e.g., `test@example.com`) ### Sign In on Device 1. On your iPhone: **Settings** → **App Store** 2. Scroll down to **Sandbox Account** 3. Sign in with your sandbox tester credentials ### Test a Purchase 1. Build your app to your physical device 2. Navigate to the paywall 3. Complete a purchase using sandbox account 4. Verify in RevenueCat dashboard → **Customers** Sandbox purchases are free and can be repeated for testing. *** ## Verification Checklist Before testing, verify: * [ ] Paid Apps Agreement is **Active** (not pending) * [ ] Products have **price** set * [ ] Products have **localization** (name + description) * [ ] Products are attached to **entitlements** in RevenueCat * [ ] Offering is set as **Current** in RevenueCat * [ ] Sandbox account is signed in on device * [ ] Bundle ID matches between app and App Store Connect *** ## Next Steps Common errors and solutions Design high-converting paywalls ## Remove / Disable To disable payments while you configure RevenueCat, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # RevenueCat Troubleshooting Source: https://docs.launchtoday.dev/payments/revenuecat-troubleshooting Common errors and solutions for RevenueCat integration This page documents common RevenueCat issues and their solutions. *** ## Products Not Loading ### "None of the products could be fetched from App Store Connect" This is the most common error. Check these in order: **Simulator cannot fetch products from App Store Connect.** Solution: Test on a physical device, or create a StoreKit Configuration file in Xcode. Go to App Store Connect → **Agreements, Tax, and Banking** * Status must be **Active** (not pending) * Complete all tax and banking info Each product in App Store Connect needs: * ✅ Price set * ✅ At least one localization (display name + description) * ✅ Subscription group assigned Your app's bundle ID must exactly match App Store Connect. Check `app.config.ts`: ```typescript theme={null} ios: { bundleIdentifier: "com.yourcompany.yourapp", } ``` On your iPhone: 1. Settings → App Store → scroll to Sandbox Account 2. Sign in with a sandbox tester Sometimes this is an **Apple-side issue** with StoreKit. If all the above checks pass: * Wait 15-30 minutes and try again * New products can take time to propagate through Apple's systems * Check [Apple System Status](https://www.apple.com/support/systemstatus/) for outages * If persistent, contact Apple Developer Support This error is often caused by Apple's StoreKit systems, not your configuration. If you've verified all settings are correct, waiting is usually the best solution. ### "None of the products registered in RevenueCat could be fetched from the Play Store" This is the Android version of “offerings empty.” Check these in order: If you haven’t set up products yet, disable payments to avoid noisy logs: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false`. Your Android package name must match Play Console: `app.config.ts` → `android.package`. In Play Console → **Monetize** → **Products**, ensure each product is Active and has pricing set. Install a signed build (EAS or Play testing). Play Store products won’t load in a debug build. Upload a build to a **closed testing** track and add your tester account. Play Billing products usually require a Play-hosted build. ### "Invalid credentials" or Play Console auth errors (Android) RevenueCat needs a Google Play **service account** with the correct permissions, and your app must be in a **closed testing** track. In Play Console → **Setup** → **API access**, create a service account and link it to your project. Download the JSON key for RevenueCat. Grant the service account access to subscriptions and financial data (RevenueCat's docs list the exact roles needed). Upload a build to **closed testing** and add your tester account. Google Play Billing won't validate products for non‑hosted builds. RevenueCat documents the exact Play Console roles and setup flow for Android. Use their guide if you need step‑by‑step permissions details. *** ## Configuration Warnings ### "RevenueCat already configured" ``` WARN RevenueCat already configured, skipping... ``` **This is harmless.** It means `Purchases.configure()` was called more than once. Our provider handles this automatically. ### "Products are configured but aren't approved in App Store Connect" ``` Products status (READY_TO_SUBMIT) requires action in App Store Connect ``` **This is expected for development.** Products in "Ready to Submit" status work for: * ✅ Sandbox testing (physical device) * ❌ Simulator testing * ❌ Production Products become fully "Approved" after your first app submission. *** ## Entitlement Issues ### "User has no active entitlements" after purchase 1. **Check product → entitlement mapping** * RevenueCat → Product Catalog → Products * Verify product is attached to an entitlement 2. **Verify purchase completed** * RevenueCat → Customers → search for user * Check transaction history 3. **Refresh subscription status** ```typescript theme={null} const { refresh } = useRevenueCat(); await refresh(); ``` ### Subscription works in sandbox but not production 1. App must be approved and live on App Store 2. Products must be approved (not "Ready to Submit") 3. Paid Apps Agreement must be active 4. User must have a real (not sandbox) purchase *** ## Paywall Issues ### Paywall shows but no products displayed 1. **Check Offerings in RevenueCat** * Product Catalog → Offerings * Ensure packages are assigned * Verify offering is set as "Current" 2. **Check paywall assignment** * Paywalls → your paywall * Verify it's assigned to an offering ### Paywall not showing at all 1. **Check RevenueCat initialization** * Look for `✅ RevenueCat initialization complete!` in logs * Verify API key is correct 2. **Check for errors in logs** * Enable verbose logging in development * Look for `❌ Failed to fetch offerings` *** ## Testing Issues ### Can't test on Simulator The iOS Simulator cannot connect to App Store Connect. Options: 1. **Use a physical device** (recommended) 2. **Create a StoreKit Configuration file**: * In Xcode: File → New → File → StoreKit Configuration File * Add products with matching IDs * Edit Scheme → Run → Options → StoreKit Configuration ### Sandbox purchases not working 1. **Sign out of production App Store** * Settings → App Store → sign out of your real Apple ID 2. **Sign into sandbox account** * Settings → App Store → Sandbox Account 3. **Use a fresh sandbox account** * App Store Connect → Users & Access → Sandbox → create new ### "Cannot connect to iTunes Store" * Check internet connection * Try a different sandbox account * Wait a few minutes and retry (App Store Connect can have delays) *** ## API & Network Issues ### "Network request failed" 1. Check internet connection 2. Verify API key is correct 3. Check RevenueCat status page: [status.revenuecat.com](https://status.revenuecat.com) ### Customer info not updating ```typescript theme={null} // Force refresh from network const { refresh } = useRevenueCat(); await refresh(); ``` *** ## Debug Logging Enable verbose logging to see detailed RevenueCat activity: ```typescript theme={null} // In provider.tsx - already enabled in development if (__DEV__) { Purchases.setLogLevel(LOG_LEVEL.VERBOSE); } ``` Look for these log patterns: | Log | Meaning | | ------------------------------------ | ---------------------------- | | `✅ RevenueCat configured` | SDK initialized successfully | | `✅ Loaded X products` | Products fetched from store | | `ℹ️ User has no active entitlements` | User hasn't purchased | | `❌ Failed to fetch offerings` | Products couldn't be loaded | *** ## Still Stuck? 1. **Check RevenueCat's official docs**: [docs.revenuecat.com](https://docs.revenuecat.com) 2. **RevenueCat community**: [community.revenuecat.com](https://community.revenuecat.com) 3. **Check error link**: Most errors include a URL with detailed info *** ## Next Steps Review the setup steps Official documentation ## Remove / Disable To disable payments while you configure RevenueCat, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # Stripe Source: https://docs.launchtoday.dev/payments/stripe Accept payments, manage subscriptions, and handle billing with Stripe # Stripe Integration Stripe is the payment provider used for direct billing in Launch. It handles one-time purchases, subscriptions, and customer management, while the app and API coordinate paywalls, checkout, and entitlement updates. Launch includes a production-ready Stripe integration with a paywall screen, webhook handling, and subscription management. This gives you a full billing flow without wiring everything from scratch. Stripe Paywall Screen ## What's Included Beautiful, customizable paywall with support for yearly, monthly, and one-time purchases. Native payment buttons for faster checkout on iOS and Android. Complete webhook setup for subscription events, payment success/failure, and more. Type-safe API endpoints for creating payment intents, checkout sessions, and managing subscriptions. Prisma models for customers, subscriptions, and payment history. Reusable selection card component matching the app's design system. ## Paywall Features The included paywall screen lives in `apps/mobile/app/payments/stripe.tsx` and supports: * **Multiple pricing tiers** - Yearly, monthly, and lifetime options * **Dynamic pricing** - Pulls products directly from your Stripe dashboard * **Apple Pay & Google Pay** - Native payment buttons for faster checkout * **Dark mode** - Fully themed for light and dark modes * **Customizable content** - Easy to edit title, benefits, and branding * **Terms & Privacy links** - Required for App Store compliance * **Haptic feedback** - Native feel with tactile responses * **Reusable SelectCard component** - Consistent design across your app ## Setup Requirements The Stripe paywall only shows the “setup required” state when: * The Stripe env vars are missing, **or** * The Stripe product list loads successfully but contains zero products. If the API request fails, the screen will display a separate error message so you can distinguish missing config from backend/API issues. ## Quick Start Create products in your Stripe dashboard and configure environment variables. [View Setup Guide →](/payments/stripe-setup) Set up webhook endpoints to handle subscription events. [View Webhook Guide →](/payments/stripe-webhooks) Update the paywall copy and pricing labels in `apps/mobile/app/payments/stripe.tsx` to match your branding. ## File Structure Key integration points: * API endpoints: `apps/api/src/routers/stripe.ts` * Webhook handler: `apps/api/src/routes/stripe-webhooks.ts` * Stripe client: `apps/api/src/lib/stripe.ts` * Paywall UI: `apps/mobile/app/payments/stripe.tsx` ## Webhooks The API includes webhook handling for Stripe events like subscription updates, payment success/failure, and customer lifecycle changes. See `apps/api/src/routes/stripe-webhooks.ts` for the event coverage. ## Database schema Stripe data is stored in Prisma models dedicated to customers, subscriptions, and payment history. Review the models in `apps/api/prisma/schema.prisma` and the reference guide at `/payments/database-schema`. ## Customizing the paywall The paywall content and layout live in `apps/mobile/app/payments/stripe.tsx`. Update the copy, pricing labels, and terms/privacy links to match your brand. ## Next Steps Environment variables, ngrok, and product creation. Handle subscription events and payment notifications. tRPC endpoints for payments and subscriptions. Prisma models for payment data. # Stripe API Source: https://docs.launchtoday.dev/payments/stripe-api tRPC endpoints for Stripe payments and subscriptions # Stripe API Launch provides type-safe tRPC endpoints for all Stripe operations. These endpoints handle payment intents, checkout sessions, and product management. ## Available Endpoints ### `stripe.getProducts` Fetches all active products and prices from your Stripe account. ```typescript theme={null} const { data } = trpc.stripe.getProducts.useQuery(); // Returns { products: [ { id: "prod_xxx", name: "Pro Yearly", description: "Annual subscription", prices: [ { id: "price_xxx", unit_amount: 9999, recurring: { interval: "year" } }, ], }, ]; } ``` ### `stripe.createPaymentIntent` Creates a payment intent for one-time purchases. ```typescript theme={null} const mutation = trpc.stripe.createPaymentIntent.useMutation(); const { clientSecret } = await mutation.mutateAsync({ amount: 999, // Amount in cents currency: "usd", description: "Pro Lifetime Access", }); ``` ### `stripe.createCheckoutSession` Creates a checkout session for subscriptions. ```typescript theme={null} const mutation = trpc.stripe.createCheckoutSession.useMutation(); const { url } = await mutation.mutateAsync({ priceId: "price_xxx", mode: "subscription", successUrl: "myapp://success", cancelUrl: "myapp://cancel", }); ``` ### `stripe.getSubscriptionStatus` Returns the current Stripe subscription for the authenticated user (if any), plus the local subscription record used for display. ```typescript theme={null} const { data } = trpc.stripe.getSubscriptionStatus.useQuery(); // Returns { hasSubscription: true, subscription: { id: "sub_xxx", status: "active", cancel_at_period_end: false }, localSubscription: { id: "sub_xxx", status: "active", cancelAtPeriodEnd: false, currentPeriodEnd: "2026-01-23T12:34:56.000Z", }, } ``` ### `stripe.cancelSubscription` Schedules a subscription to cancel at period end. ```typescript theme={null} const mutation = trpc.stripe.cancelSubscription.useMutation(); await mutation.mutateAsync(); ``` ### `stripe.resumeSubscription` Resumes a subscription that was scheduled to cancel at period end. ```typescript theme={null} const mutation = trpc.stripe.resumeSubscription.useMutation(); await mutation.mutateAsync(); ``` ### `stripe.changeSubscriptionPlan` Switches between subscription prices (monthly ↔ annual). Upgrades take effect immediately with proration; downgrades take effect at renewal. ```typescript theme={null} const mutation = trpc.stripe.changeSubscriptionPlan.useMutation(); await mutation.mutateAsync({ priceId: "price_new", }); ``` ## API Structure **Location:** `apps/api/src/routers/stripe.ts` ```typescript theme={null} export const stripeRouter = router({ getProducts: publicProcedure.query(async () => { // Fetch products from Stripe }), createPaymentIntent: protectedProcedure .input( z.object({ amount: z.number(), currency: z.string(), description: z.string().optional(), }) ) .mutation(async ({ input, ctx }) => { // Create payment intent }), createCheckoutSession: protectedProcedure .input( z.object({ priceId: z.string(), mode: z.enum(["subscription", "payment"]), successUrl: z.string(), cancelUrl: z.string(), }) ) .mutation(async ({ input, ctx }) => { // Create checkout session }), getSubscriptionStatus: protectedProcedure.query(async ({ ctx }) => { // Return Stripe subscription and local subscription record }), cancelSubscription: protectedProcedure.mutation(async ({ ctx }) => { // Set cancel_at_period_end to true }), resumeSubscription: protectedProcedure.mutation(async ({ ctx }) => { // Set cancel_at_period_end to false }), changeSubscriptionPlan: protectedProcedure .input(z.object({ priceId: z.string() })) .mutation(async ({ input, ctx }) => { // Swap subscription price with proration rules }), }); ``` ## Authentication * `getProducts` - Public, no authentication required * `createPaymentIntent` - Protected, requires authenticated user * `createCheckoutSession` - Protected, requires authenticated user * `getSubscriptionStatus` - Protected, requires authenticated user * `cancelSubscription` - Protected, requires authenticated user * `resumeSubscription` - Protected, requires authenticated user * `changeSubscriptionPlan` - Protected, requires authenticated user ## Customer Management When a user makes their first payment, a Stripe customer is automatically created and linked to their account: ```typescript theme={null} // Automatic customer creation const customer = await stripe.customers.create({ email: user.email, name: user.name, metadata: { userId: user.id }, }); ``` ## Error Handling All endpoints return structured errors: ```typescript theme={null} try { await mutation.mutateAsync({ ... }); } catch (error) { // error.message contains Stripe error details Alert.alert("Payment Failed", error.message); } ``` ## Test Checklist * `stripe.getProducts` returns products * `stripe.createPaymentIntent` returns a client secret * `stripe.createCheckoutSession` returns a valid URL * `stripe.getSubscriptionStatus` returns subscription info (if subscribed) * `stripe.cancelSubscription` sets `cancel_at_period_end` * `stripe.resumeSubscription` clears `cancel_at_period_end` * `stripe.changeSubscriptionPlan` swaps price with correct proration ## Troubleshooting If API calls fail, verify Stripe keys in `apps/api/.env` (copy from `apps/api/example.env` first) and restart the API. ## Remove / Disable To disable payments while you configure Stripe, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). ## Next Steps Handle asynchronous payment events. View the database models for payment data. # Stripe Setup Source: https://docs.launchtoday.dev/payments/stripe-setup Complete setup guide for Stripe integration including ngrok, webhooks, products, and environment variables # Complete Stripe Setup Guide This guide walks you through setting up Stripe integration for the Launch payment system, including local development with ngrok, webhook configuration, and creating the required products. ## Prerequisites 1. Stripe account (free at [stripe.com](https://stripe.com)) 2. ngrok installed for local webhook testing 3. Launch development environment running (API on port 3001) ## Step 1: Setup ngrok for Local Development First, set up ngrok to expose your local API for webhook testing: ### Install ngrok ```bash theme={null} # Install ngrok brew install ngrok/ngrok/ngrok # or download from https://ngrok.com/download ``` ### Start ngrok tunnel ```bash theme={null} # Expose your local API (port 3001) ngrok http 3001 ``` You'll get a URL like: `https://abc123.ngrok-free.app` **Important**: Keep this terminal running throughout development. ## Step 2: Create Stripe Products Create these exact products in your Stripe Dashboard: ### Option A: Using Stripe Dashboard (Recommended) 1. Go to [Stripe Dashboard → Products](https://dashboard.stripe.com/products) 2. Click "Add product" #### Product 1: Upload Pack 100 * **Name**: `Upload Pack 100` * **Description**: `Adds 100 extra document uploads to your account.` * **Pricing**: * **Price**: `$4.99` * **Billing**: `One time` * **Copy the Product ID**: `prod_...` (you'll need this) #### Product 2: Launch Pro * **Name**: `Launch Pro` * **Description**: `Unlocks unlimited doc uploads and AI Pro features. Includes customer portal access.` * **Pricing**: * **Price**: `$9.99` * **Billing**: `Monthly recurring` * **Copy the Product ID**: `prod_...` (you'll need this) ### Option B: Using Stripe CLI ```bash theme={null} # Create Upload Pack product stripe products create \ --name="Upload Pack 100" \ --description="Adds 100 extra document uploads to your account." \ --type=service # Create Launch Pro product stripe products create \ --name="Launch Pro" \ --description="Unlocks unlimited doc uploads and AI Pro features. Includes customer portal access." \ --type=service # Create prices (replace prod_xxx with actual product IDs) stripe prices create \ --currency=usd \ --unit-amount=499 \ --product=prod_xxx stripe prices create \ --currency=usd \ --unit-amount=999 \ --recurring[interval]=month \ --product=prod_xxx ``` ## Step 3: Setup Webhook Endpoint ### Create webhook in Stripe Dashboard 1. Go to [Stripe Dashboard → Webhooks](https://dashboard.stripe.com/webhooks) 2. Click "Add endpoint" 3. **Endpoint URL**: `https://your-ngrok-url.ngrok-free.app/webhooks/stripe` 4. **Events to select**: * `customer.subscription.created` * `customer.subscription.updated` * `customer.subscription.deleted` * `invoice.payment_succeeded` * `invoice.payment_failed` * `payment_intent.succeeded` * `payment_intent.payment_failed` 5. Click "Add endpoint" 6. **Copy the Webhook Secret**: `whsec_...` (you'll need this) ## Step 4: Environment Variables ### Backend Configuration **File**: `apps/api/.env` (copy from `apps/api/example.env` first) ```bash theme={null} # Stripe Configuration STRIPE_SECRET_KEY=sk_test_51... # From Stripe Dashboard → API Keys STRIPE_WEBHOOK_SECRET=whsec_... # From webhook endpoint you just created # Database (if not already set) DATABASE_URL="postgresql://..." ``` ### Mobile App Configuration **File**: `apps/mobile/.env` (copy from `apps/mobile/example.env` first) ```bash theme={null} # Stripe Publishable Key (safe for client-side) EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51... ``` **File**: `apps/mobile/app.config.ts` Make sure this section exists: ```typescript theme={null} export default { // ... other config plugins: [ // ... other plugins [ "@stripe/stripe-react-native", { merchantIdentifier: "merchant.com.yourcompany.yourapp", enableGooglePay: true, }, ], ], }; ``` ## Step 5: Enable Apple Pay (Optional) Apple Pay provides a faster checkout experience for iOS users. Follow these steps to enable it: ### 5.1 Create Merchant ID in Apple Developer Portal 1. Go to [Apple Developer → Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/merchant) 2. Click the **+** button to add a new identifier 3. Select **Merchant IDs** and click Continue 4. Enter your Merchant ID (e.g., `merchant.com.yourcompany.yourapp`) 5. Click Register ### 5.2 Get Certificate Signing Request from Stripe 1. Go to [Stripe Dashboard → Settings → Payment Methods → Apple Pay](https://dashboard.stripe.com/settings/payments/apple_pay) 2. Click **Add new application** 3. Download the **Certificate Signing Request (CSR)** file that Stripe provides 4. Save the `.certSigningRequest` file ### 5.3 Create Apple Pay Certificate 1. Go back to [Apple Developer → Certificates](https://developer.apple.com/account/resources/certificates/list) 2. Click the **+** button to create a new certificate 3. Select **Apple Pay Merchant Identity Certificate** 4. Select your Merchant ID from the dropdown 5. Click **Choose File** and upload the CSR file from Stripe 6. Click Continue and then Download the certificate ### 5.4 Upload Certificate to Stripe 1. Go back to [Stripe Dashboard → Apple Pay settings](https://dashboard.stripe.com/settings/payments/apple_pay) 2. Upload the `.cer` certificate file you downloaded from Apple 3. Click Submit ### 5.5 Update app.config.ts Make sure your merchant identifier matches: ```typescript theme={null} [ "@stripe/stripe-react-native", { merchantIdentifier: "merchant.com.yourcompany.yourapp", // Must match Apple Developer enableGooglePay: true, }, ], ``` ### 5.6 Rebuild the App Apple Pay requires a native rebuild: ```bash theme={null} cd apps/mobile npx expo prebuild --clean npx expo run:ios ``` Apple Pay only works on real devices, not simulators. You'll need to test on a physical iPhone with Apple Pay configured. ## Step 6: Restart Your Application After setting up environment variables: ### Restart API Server ```bash theme={null} cd apps/api npm run dev ``` ### Restart Mobile App ```bash theme={null} cd apps/mobile npx expo start --clear ``` ## Step 7: Test the Integration ### Test Webhook Connection 1. **Check API logs**: You should see Stripe environment logs: ``` 🔑 Stripe environment check: STRIPE_SECRET_KEY exists: true ``` 2. **Test webhook endpoint** in your browser: ``` https://your-ngrok-url.ngrok-free.app/webhooks/stripe ``` You should see: `{"error": "Webhook endpoint not found"}` (this is expected for GET requests) ### Test Payment Flow 1. **Open mobile app** → Navigate to Payments screen 2. **Provider Status** should show "Test" with auto-detected setup 3. **Click "Run Test Payment"** → Choose "Upload Pack (\$4.99)" 4. **Complete payment** using test card: `4242 4242 4242 4242` 5. **Check Stripe Dashboard** → You should see: * Payment succeeded * Customer created with your name/email * Webhook events fired ### Verify Webhook Events In your API logs, you should see: ``` 🔔 Stripe webhook received: payment_intent.succeeded 💳 Payment succeeded: pi_xxx - $4.99 (TEST) 💾 Test payment saved to database for user: usr_xxx ``` ## Step 8: Troubleshooting ### Common Issues #### "Webhook signature verification failed" * ✅ Check `STRIPE_WEBHOOK_SECRET` matches your webhook endpoint * ✅ Make sure ngrok URL is correct in webhook settings * ✅ Restart API server after changing environment variables #### "You did not provide an API key" * ✅ Check `STRIPE_SECRET_KEY` is set in `apps/api/.env` * ✅ Restart API server after adding the key #### "Payment failed" on mobile * ✅ Check `EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY` is set * ✅ Restart Expo app with `--clear` flag * ✅ Try test card: `4242 4242 4242 4242` #### Webhook events not received * ✅ Check ngrok is running and URL is correct * ✅ Test webhook endpoint manually in browser * ✅ Check Stripe webhook logs in dashboard ### Debug Commands ```bash theme={null} # Check if environment variables are loaded cd apps/api && node -e "console.log(process.env.STRIPE_SECRET_KEY?.substring(0, 10))" # Test ngrok connection curl https://your-ngrok-url.ngrok-free.app/health # Check webhook endpoint curl https://your-ngrok-url.ngrok-free.app/webhooks/stripe ``` ## Step 9: Production Deployment When ready for production: ### 1. Production Webhook Endpoint 1. Deploy your API to production (e.g., Vercel, Railway, etc.) 2. Create a new webhook endpoint with your production URL: ``` https://your-production-api.com/webhooks/stripe ``` 3. Copy the new webhook secret ### 2. Production Environment Variables Update your production environment with: ```bash theme={null} # Production Stripe keys STRIPE_SECRET_KEY=sk_live_... STRIPE_WEBHOOK_SECRET=whsec_... # Production database DATABASE_URL="postgresql://production..." ``` ### 3. Switch to Live Mode In Stripe Dashboard: 1. Toggle from "Test mode" to "Live mode" 2. Update webhook endpoints 3. Get live API keys ## Test Cards for Development Use these Stripe test cards: * **Success**: `4242 4242 4242 4242` * **Decline**: `4000 0000 0000 0002` * **3D Secure**: `4000 0000 0000 3220` * **Insufficient funds**: `4000 0000 0000 9995` **CVV**: Any 3 digits\ **Expiry**: Any future date ## Security Best Practices ✅ **Never expose secret keys** in client code\ ✅ **Always verify webhook signatures** (already implemented)\ ✅ **Use HTTPS** for all webhook endpoints\ ✅ **Validate user ownership** of subscriptions\ ✅ **Handle webhook idempotency** (duplicate events) ## What's Next? After completing this setup, you can: 1. **Build Pricing Screen** - Let users choose plans 2. **Add Billing Management** - Customer portal integration 3. **Review Paywall Best Practices** - [Paywall Best Practices](/payments/paywall-best-practices) ## Getting Help If you run into issues: 1. **Check the logs** - Both API and ngrok show helpful errors 2. **Stripe Dashboard** - View webhook delivery logs 3. **Test webhook endpoint** - Use browser or curl 4. **Verify environment variables** - Restart servers after changes Your Stripe integration should now be fully functional! 🎉 ## Remove / Disable To disable payments while you configure Stripe, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). # Stripe Webhooks Source: https://docs.launchtoday.dev/payments/stripe-webhooks Handle Stripe webhook events for subscriptions and payments # Stripe Webhooks Webhooks allow Stripe to notify your app when events happen—like successful payments, subscription changes, or failed charges. ## Supported Events Launch handles these webhook events out of the box: | Event | Description | | ------------------------------- | ---------------------------------------- | | `payment_intent.succeeded` | Payment was successful | | `payment_intent.payment_failed` | Payment failed | | `customer.subscription.created` | New subscription started | | `customer.subscription.updated` | Subscription changed (upgrade/downgrade) | | `customer.subscription.deleted` | Subscription cancelled | | `invoice.payment_succeeded` | Recurring payment successful | | `invoice.payment_failed` | Recurring payment failed | ## How It Works ``` Stripe Event → Your API → Database Update → App Response ``` 1. **Stripe sends event** to your webhook endpoint 2. **API verifies signature** to ensure authenticity 3. **Handler processes event** and updates database 4. **User sees changes** reflected in the app ## Webhook Endpoint **Location:** `apps/api/src/routes/stripe-webhooks.ts` ```typescript theme={null} // Webhook endpoint: POST /webhooks/stripe export async function handleStripeWebhook(req, res) { const sig = req.headers["stripe-signature"]; const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); switch (event.type) { case "payment_intent.succeeded": // Handle successful payment break; case "customer.subscription.created": // Handle new subscription break; // ... more handlers } } ``` ## Local Development For local testing, use ngrok to expose your API: ```bash theme={null} # Start ngrok tunnel ngrok http 3001 # Your webhook URL will be: # https://abc123.ngrok-free.app/webhooks/stripe ``` Remember to update your webhook URL in Stripe Dashboard whenever your ngrok URL changes. ## Database Updates When webhook events are received, the following tables are updated: * **`StripeCustomer`** - Customer information * **`Subscription`** - Subscription status and details * **`Payment`** - Payment history ## Debugging Check your API logs for webhook activity: ``` 🔔 Stripe webhook received: payment_intent.succeeded 💳 Payment succeeded: pi_xxx - $19.99 💾 Payment saved to database for user: usr_xxx ``` ## Test Checklist * Webhook endpoint responds to Stripe test events * `STRIPE_WEBHOOK_SECRET` matches the dashboard * Subscription/payment records update in the database ## Troubleshooting If events are not received, verify ngrok URL and webhook configuration in Stripe Dashboard. ## Remove / Disable To disable payments while you configure Stripe, set: `apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false` For production removal guidance, see [Removing Features](/essentials/removing-features). ## Next Steps Configure webhook endpoints and secrets. View the database models for payment data. # Superwall Overview Source: https://docs.launchtoday.dev/payments/superwall Paywall A/B testing and optimization to maximize subscription revenue Superwall is a paywall optimization platform that helps you maximize subscription revenue through A/B testing, remote configuration, and analytics. ## What's Included Launch includes a ready-to-use Superwall integration: * **Superwall Provider** - Context for paywall state * **Placement System** - Trigger paywalls at specific points * **Remote Paywalls** - Design in dashboard, no code changes * **A/B Testing** - Test different paywall variants ## Why Superwall? Test paywall variations to find what converts best. Change paywalls remotely without shipping app updates. Deep insights into what drives subscriptions. Trigger paywalls at strategic moments in your app. ## How It Works ``` Your App Superwall App Store ───────── ────────── ───────── Calls Placement ──▶ Shows Paywall ──▶ Handles Purchase (from dashboard) via StoreKit ``` Unlike RevenueCat where you fetch products and build a paywall, Superwall handles everything: 1. **Your App** - Calls a placement (e.g., "upgrade\_tapped") 2. **Superwall** - Shows the paywall configured for that placement 3. **App Store** - Handles the actual purchase via StoreKit ## Usage ### Show Paywall The template includes a demo in the Payments screen. Tapping "Superwall" presents the paywall immediately. ```tsx theme={null} import { usePlacement } from "expo-superwall"; function MyComponent() { const { registerPlacement } = usePlacement({ onDismiss: () => console.log("Paywall dismissed"), }); const showPaywall = () => { registerPlacement({ placement: "campaign_trigger", feature: () => { // Called if user has access (already subscribed) }, }); }; return