# AI Chat
Source: https://docs.launchtoday.dev/ai-features/overview
Build intelligent conversational experiences with multi-provider AI support
## 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 AndroidRequired sizes for each device typeTitle, subtitle, description, keywordsRequired URL to your privacy policyContact 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 deploymentApply 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}
NSPhotoLibraryUsageDescriptionWe need access to your photos to upload them.NSCameraUsageDescriptionWe 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:
If you're on Android, you should see:
## 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.
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
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
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
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
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
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.
## 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 ;
}
```
### Placements
Placements are events in your app where you want to show a paywall:
* `campaign_trigger` - Generic trigger for testing
* `feature_locked` - When user tries to access a premium feature
* `onboarding_complete` - After user finishes onboarding
In the Superwall dashboard, you link Campaigns to Placements.
## File Structure
```
apps/mobile/lib/payments/
├── config.ts # API keys and env helpers
├── superwall/
│ ├── provider.tsx # SuperwallPaymentProvider context
│ └── index.ts # Exports
└── types.ts # Shared types
apps/mobile/app/payments/
└── superwall.tsx # Paywall screen
```
## Test Checklist
* Paywall appears from the placement
* Prices display correctly (not NaN)
* Sandbox purchase succeeds
## Troubleshooting
See [Superwall Troubleshooting](/payments/superwall-troubleshooting).
## Remove / Disable
To disable Superwall 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 Superwall
Common errors and solutions
# Superwall Setup
Source: https://docs.launchtoday.dev/payments/superwall-setup
Configure Superwall for your app
This guide walks through setting up Superwall from scratch.
## Prerequisites
* Apple Developer Account
* App created in App Store Connect
* Physical iOS device (simulator can't test purchases)
***
## 1. Create Superwall Account
Go to [superwall.com](https://superwall.com) and create an account.
In the dashboard, create a new app for iOS.
Go to Settings → API Keys and copy your **iOS API Key** (starts with `pk_`).
***
## 2. Configure API Keys
Update API keys in `lib/payments/config.ts`:
```typescript theme={null}
export const SUPERWALL_CONFIG = {
iosApiKey: "pk_your_ios_api_key",
androidApiKey: "pk_your_android_api_key", // optional
};
```
***
## 2.1 Set Payment Provider
Update the feature registry:
```typescript theme={null}
export const featureFlags = {
payments: { enabled: true, provider: "superwall" },
// ...
};
```
***
## 3. Create Products in App Store Connect
Superwall uses StoreKit to fetch product prices. You need products in App Store Connect.
App Store Connect → Your App → Subscriptions → Create Group Example:
"Premium Subscriptions"
Add products to the group: - **Product ID**: `your_app_monthly_sub` -
**Price**: Set your price - **Duration**: 1 Month Repeat for yearly, etc.
Each product needs at least one localization: - Display Name: "Monthly
Premium" - Description: "Access all features"
Products should show "Ready to Submit" status.
Products won't work until they're at least "Ready to Submit". New products
can take 15-30 minutes to propagate.
***
## 4. Add Products to Superwall
Superwall Dashboard → Products → Add Products
Add each product ID exactly as it appears in App Store Connect: -
`your_app_monthly_sub` - `your_app_yearly_sub`
***
## 5. Create a Paywall
Dashboard → Paywalls → Create PaywallPick a template or start from scratch.
Drag your products onto the paywall. Configure: - Primary product (yearly
recommended) - Secondary product (monthly)
Customize the design, copy, and images.Save the paywall.
***
## 6. Create a Campaign
Campaigns link paywalls to placements.
Dashboard → Campaigns → Create Campaign
Enter the placement name your app will call: - `campaign_trigger` (default
in template) - Or create your own: `upgrade_tapped`, `feature_locked`, etc.
Select the paywall you created.
Set the campaign to **Active** (not Draft).
Campaigns in Draft status won't show paywalls!
***
## 7. Test
Run the app on a physical device (not simulator).
On device: Settings → App Store → Sandbox Account → Sign in with test
account.
Go to Payments → Superwall in the app.
* Paywall should appear with correct products - Prices should display (not
NaN) - Purchase flow should work
***
## Checklist
Before going live, verify:
* [ ] API key configured in `config.ts`
* [ ] Products created in App Store Connect
* [ ] Products added to Superwall dashboard
* [ ] Paywall designed with products
* [ ] Campaign created and **Active**
* [ ] Placement name matches code (`campaign_trigger`)
* [ ] Tested on physical device with sandbox account
***
## Next Steps
Learn about Superwall features
Common errors and solutions
## Remove / Disable
To disable payments while you configure Superwall, set:
`apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false`
For production removal guidance, see [Removing Features](/essentials/removing-features).
# Superwall Troubleshooting
Source: https://docs.launchtoday.dev/payments/superwall-troubleshooting
Common errors and solutions for Superwall integration
This page documents common Superwall issues and their solutions.
***
## Prices Showing as NaN
This means Superwall couldn't fetch product prices from StoreKit.
Products must be "Ready to Submit" or "Approved" in App Store Connect. Use
the debug script to check status: `bash node scripts/debug-appstore.js `
Product IDs in Superwall dashboard must **exactly** match App Store Connect.
Common mistakes: - Extra spaces - Wrong capitalization - Typos
Simulator cannot fetch real products from StoreKit. Use a physical device
with a sandbox account signed in.
New products can take 15-30 minutes to propagate through Apple's systems.
Check [Apple System Status](https://www.apple.com/support/systemstatus/) for
outages.
This is often an Apple StoreKit issue, not Superwall. If all settings are
correct, waiting usually resolves it.
***
## PlacementNotFound
The placement name in your code doesn't match any active Campaign in Superwall.
### Solutions
1. **Check Campaign Status**
Dashboard → Campaigns → Verify status is **Active** (not Draft)
2. **Verify Placement Name**
The placement in your code must exactly match the Campaign:
```typescript theme={null}
// In your code
registerPlacement({ placement: "campaign_trigger" });
// Must match Campaign's placement in Superwall dashboard
```
3. **Restart App**
After making changes in the dashboard, fully restart the app (not just reload).
***
## Old Paywall Showing
Superwall caches paywall assignments. To see a new paywall:
Dashboard → Campaigns → Archive the old campaign
Create a new Campaign with the new paywall
Fully quit and restart the app
Superwall uses "sticky" assignments - users see the same paywall variant
consistently. Creating a new Campaign resets this.
***
## Paywall Not Appearing
### Check SuperwallProvider
Ensure your component is wrapped with the provider:
```typescript theme={null}
import { SuperwallProvider } from "expo-superwall";
import { SUPERWALL_CONFIG } from "@/lib/payments/config";
export default function Screen() {
return (
);
}
```
### Check API Key
Verify the API key is correct and starts with `pk_`.
### Check Campaign Status
Dashboard → Campaigns → Must be **Active**
### Check Console Logs
Look for errors in the console when triggering the placement.
***
## "Cannot connect to iTunes Store"
This is an Apple/StoreKit error, not Superwall.
1. Check internet connection
2. Try a different sandbox account
3. Wait a few minutes and retry
4. Check [Apple System Status](https://www.apple.com/support/systemstatus/)
***
## Simulator Testing
**Simulators cannot connect to App Store Connect.**
Options:
1. **Use Physical Device** (recommended)
* Sign into sandbox account in Settings → App Store
2. **Use StoreKit Configuration File**
* Xcode → File → New → StoreKit Configuration File
* Add products with matching IDs
* Edit Scheme → Run → Options → StoreKit Configuration
***
## Multiple Navigations Back
If closing the paywall navigates back multiple screens:
This happens when multiple callbacks fire. The template includes a guard:
```typescript theme={null}
const hasNavigatedBack = useRef(false);
const goBack = useCallback(() => {
if (!hasNavigatedBack.current) {
hasNavigatedBack.current = true;
router.back();
}
}, [router]);
```
Ensure your paywall screen uses this pattern.
***
## Superwall vs RevenueCat
| Feature | Superwall | RevenueCat |
| ----------------------- | -------------------- | ----------------- |
| Paywall A/B Testing | ✅ Built-in | ❌ Manual |
| Remote Paywall Updates | ✅ Yes | ✅ Yes (Paywalls) |
| Subscription Management | ❌ No | ✅ Yes |
| Cross-Platform Sync | ❌ No | ✅ Yes |
| Analytics | ✅ Conversion focused | ✅ Revenue focused |
**Use Superwall if**: You want to optimize paywall conversion with A/B testing
**Use RevenueCat if**: You need cross-platform subscription management
**Use Both**: Some apps use Superwall for paywalls and RevenueCat for subscription management
***
## Still Stuck?
Official documentation
NPM package docs
## Remove / Disable
To disable payments while you configure Superwall, set:
`apps/mobile/features/feature-registry.tsx` → `featureFlags.payments = false`
For production removal guidance, see [Removing Features](/essentials/removing-features).
# Project Structure
Source: https://docs.launchtoday.dev/project-structure/index
How the Launch monorepo is organized
## Overview
Launch is a Turborepo monorepo that uses a single `pnpm` workspace to manage
dependencies across apps. A monorepo keeps related code in one place so you can
share types, reuse utilities, and ship features across the API and mobile app
without cross-repo overhead.
At the top level, `apps/` holds the product apps (mobile, api, and web — work in progress).
The root config files (`package.json`, `pnpm-workspace.yaml`, `turbo.json`) manage
dependencies and task orchestration across the workspace.
`pnpm-workspace.yaml` defines the workspace packages and `turbo.json` runs tasks
across them efficiently.
## Apps
iOS and Android app built with Expo SDK 55 and Expo Router.
Backend server with tRPC, Better Auth, and Prisma.
Web app is planned, but not shipped yet.
***
## Mobile App
The mobile app uses Expo Router for file-based navigation. Screens go in `app/`, reusable components in `components/`.
```
apps/mobile/
├── app/
│ ├── _layout.tsx
│ ├── index.tsx
│ ├── (tabs)/ # Tab screens
│ ├── onboarding/ # Onboarding flow
│ └── auth/ # Auth screens
├── components/ # Reusable UI components
├── features/ # Feature modules (chat, etc)
├── lib/ # API client, hooks, utilities
├── constants/ # Colors, spacing, typography
├── assets/ # Fonts, images
├── app.config.ts
└── package.json
```
| Folder | What goes here |
| ------------- | ------------------------------------------------------------------- |
| `app/` | Screens and navigation. File name = route. |
| `components/` | Shared UI components used across screens. |
| `features/` | Self-contained feature modules with their own hooks and components. |
| `lib/` | API client, authentication, utilities, custom hooks. |
| `constants/` | Theme tokens, colors, spacing values. |
***
## API
The API uses Koa.js as the server framework with tRPC for type-safe endpoints and Prisma for database access.
The API source lives under `apps/api/src/` (routes, routers, services, middleware,
and shared clients), while `apps/api/prisma/` contains the database schema and
migrations.
| Folder | What goes here |
| ----------- | ------------------------------------------------------------ |
| `routers/` | tRPC routers. Each file exports procedures for a domain. |
| `routes/` | Traditional REST endpoints like auth callbacks and webhooks. |
| `services/` | Business logic. Keep routers thin, put logic here. |
| `lib/` | Database client, external service clients (Stripe, Twilio). |
| `prisma/` | Database schema and migrations. |
***
## Key Files
| File | Purpose |
| ------------------------------- | ------------------------------------------------- |
| `apps/mobile/app.config.ts` | Expo configuration (app name, bundle ID, plugins) |
| `apps/mobile/app/_layout.tsx` | Root layout with auth provider and navigation |
| `apps/api/src/router.ts` | Main tRPC router combining all sub-routers |
| `apps/api/prisma/schema.prisma` | Database models and relationships |
| `turbo.json` | Turborepo build pipeline configuration |
| `pnpm-workspace.yaml` | Workspace package definitions |
## Troubleshooting
* **Can’t find a screen**: check `apps/mobile/app/`
* **Missing API route**: check `apps/api/src/router.ts` for registration
## Next Steps
Run the API and database locally.
Launch Expo and open the app.
# Android (FCM)
Source: https://docs.launchtoday.dev/push-notifications/android
Configure Firebase Cloud Messaging for Android pushes
## Overview
Android push notifications use Firebase Cloud Messaging (FCM). The app registers a native FCM token, stores it in `device_tokens`, and the API sends messages via the FCM HTTP v1 API.
Key pieces in this repo:
* `apps/mobile/lib/notifications.ts`
* `apps/mobile/app/onboarding/push-notifications.tsx`
* `apps/api/src/lib/push/fcm.ts`
* `apps/api/src/routes/api/push.ts`
* `apps/api/src/routers/push.ts`
## Client Setup (Android)
1. Download `google-services.json` from the Firebase console for the Android app.
2. Place it at:
```
apps/mobile/lib/google-services.json
```
3. Confirm `app.config.ts` points to it:
```
android: {
googleServicesFile: "./lib/google-services.json"
}
```
4. Replace placeholder values in `app.config.ts` (package name / bundle ID, URL
schemes, EAS project ID, Stripe merchant identifier) to match your app.
5. Rebuild the native project (required after adding `google-services.json`):
```
pnpm -C apps/mobile prebuild
pnpm -C apps/mobile android
```
Notes:
* FCM does not work in Expo Go. Use a dev client or a full build.
* The app creates a notification channel on Android before requesting the token.
## Why this file is required
`google-services.json` is consumed by the Android build tooling to configure
Firebase/FCM. Without it, Android cannot register for a push token and FCM
pushes will fail. This repo ships a placeholder file so you can commit safely,
but you must replace it with the Firebase config for your own app.
## Backend Setup (FCM)
FCM requires a Firebase **service account** (Admin SDK) JSON. Do not use `google-services.json`.
Required env vars:
* `FCM_PROJECT_ID`
* `FCM_SERVICE_ACCOUNT_BASE64`
Base64 encode your service account JSON (macOS):
```
base64 -i /absolute/path/to/service-account.json | pbcopy
```
Then set:
```
FCM_PROJECT_ID=your-firebase-project-id
FCM_SERVICE_ACCOUNT_BASE64=
```
## Sending a Test Push
Use the API guide to create a push token and send a test notification. The API sends via APNs for iOS tokens and FCM for Android tokens.
See: [API Setup](/push-notifications/api)
## Troubleshooting
If you see:
```
Default FirebaseApp is not initialized in this process
```
Make sure:
* You rebuilt the Android native project after adding `google-services.json`.
* You are not running in Expo Go.
# API
Source: https://docs.launchtoday.dev/push-notifications/api
Create tokens and send push notifications
## Create a Push API Token
Push API tokens let you send notifications without exposing your primary auth session. They are intended for internal use, such as CLI tools or server-side scripts.
Endpoint:
```
POST /api/push/token
```
Required headers:
* `Authorization: Bearer `
Request body:
```
{
"userId": "USER_ID",
"name": "cli",
"expiresInDays": 30
}
```
Response:
```
{
"token": "",
"expiresAt": "2026-02-16T10:00:00.000Z"
}
```
### Create token via curl
```
curl -X POST "http://localhost:3001/api/push/token" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"userId": "",
"name": "cli",
"expiresInDays": 30
}'
```
## Send a Push
The send endpoint accepts a token and routes the message through APNs (iOS) or FCM (Android).
Endpoint:
```
POST /api/push/send
```
Required headers:
* `Authorization: Bearer `
How it works:
* The API token is looked up in `push_api_tokens`.
* Device tokens are loaded from `device_tokens` and filtered by user and notification preferences.
* If no `userId` is provided, it sends to the token owner.
* iOS tokens are sent via APNs and Android tokens via FCM.
Request body:
```
{
"title": "Hello",
"body": "Test push",
"data": { "screen": "settings" }
}
```
Options:
* `userId`: send to a specific user
* `all: true`: broadcast to all iOS and Android tokens
* If neither is set, it sends to the token owner
### Send push via curl
```
curl -X POST "http://localhost:3001/api/push/send" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"title": "Hello",
"body": "Test push",
"data": { "screen": "settings" }
}'
```
# Push Notifications
Source: https://docs.launchtoday.dev/push-notifications/index
Send and handle push notifications in your app
## Overview
Push notifications help you reach users when they are away from the app. They are most effective when they are timely, relevant, and respectful of user preferences. In Launch, notifications are sent directly through APNs on iOS and FCM on Android to give you full control over payloads, delivery, and routing.
## Prerequisites
* Apple Developer account (APNs)
* Firebase project (FCM)
* Development build for native entitlements
* Backend env vars set for APNs/FCM (copy from `apps/api/example.env`)
APNs (Apple Push Notification service) is Apple’s gateway for delivering pushes to iOS devices. FCM (Firebase Cloud Messaging) is Google’s gateway for Android. We send directly to both services instead of using Expo Push so we can control authentication, payload structure, delivery timing, and future routing logic without relying on a relay.
Configure APNs tokens and credentials
Configure Firebase Cloud Messaging
Create tokens and send notifications via curl
Scheduling, queues, and best practices
## How It Works
```
┌─────────────┐ ┌─────────────┐
│ Your API │────►│ APNs/FCM │
│ │ │ │
└─────────────┘ └─────────────┘
│
▼
┌──────────────────┐
│ iOS/Android Apps │
└──────────────────┘
```
1. The app requests permissions and registers a native device token.
2. The token is stored per user in `device_tokens` on the backend.
3. Your API sends a notification through APNs (iOS) or FCM (Android).
4. The device receives and displays the notification.
## API Flow (iOS + Android)
When a user opts in on the onboarding screen, the app registers a native device token and stores it in the backend. Your API then targets those tokens when sending a push. APNs uses your bundle ID as the topic, and FCM uses your Firebase project to route the message to the correct app.
Key pieces in this repo:
* `apps/mobile/app/onboarding/push-notifications.tsx`
* `apps/mobile/lib/notifications.ts`
* `apps/api/src/lib/push/apns.ts`
* `apps/api/src/lib/push/fcm.ts`
* `apps/api/src/routes/api/push.ts`
* `apps/api/src/routers/push.ts`
## Steps
1. Configure credentials:
* [iOS Setup](/push-notifications/ios)
* [Android Setup](/push-notifications/android)
2. Rebuild the app (push entitlements require a native build).
3. Use the onboarding screen to register a device token.
4. Send a test push from your API: [API Setup](/push-notifications/api)
## Test Checklist
* iOS device receives a test push
* Android device receives a test push
* Token is stored on the backend for the signed-in user
## Troubleshooting
Start with platform setup guides, then see [Troubleshooting](/troubleshooting).
## Remove / Disable
If you are not shipping push, skip the onboarding step and remove token
registration in `apps/mobile/lib/notifications.ts`. For production removal
guidance, see [Removing Features](/essentials/removing-features).
## Best Practices
* Ask for permission only when there is clear value.
* Keep payloads short and actionable.
* Avoid excessive sends; let users control frequency.
* Use targeting for relevance instead of broadcasting by default.
* For scheduling and queueing strategies, see [Guidance](/push-notifications/guidance).
## Next Steps
* [iOS Setup](/push-notifications/ios)
* [Android Setup](/push-notifications/android)
# iOS (APNs)
Source: https://docs.launchtoday.dev/push-notifications/ios
Configure APNs for direct push notifications
## Requirements
Before you can send pushes, you need an APNs key tied to your Apple Developer account and a build that includes the push entitlement. APNs uses your bundle ID to route notifications to the correct app.
## Configure APNs
1. Apple Developer Portal → Certificates, Identifiers & Profiles → Keys
2. Create a new key with **Apple Push Notifications service (APNs)**
3. Download the `.p8` file (only available once)
4. Note your **Key ID** and **Team ID**
## Environment Variables (API)
Set these on the API server:
```
APNS_TEAM_ID=YOUR_TEAM_ID
APNS_KEY_ID=YOUR_KEY_ID
APNS_BUNDLE_ID=com.launchhq.mobile
APNS_PRODUCTION=false
APNS_KEY_BASE64=...base64 of .p8...
```
Notes:
* Use `APNS_PRODUCTION=false` for dev builds.
* Use `APNS_PRODUCTION=true` for TestFlight/App Store builds.
## iOS Entitlement
Ensure APNs entitlement is present in `apps/mobile/app.config.ts`:
```
ios: {
entitlements: {
"aps-environment": "development",
},
}
```
Rebuild the iOS app after changing entitlements.
## Device Token Registration
The app registers a native device token when the user taps **Enable Notifications**:
* `apps/mobile/lib/notifications.ts`
* `apps/mobile/app/onboarding/push-notifications.tsx`
The token is stored in the backend under `device_tokens` and used when your API sends through APNs.
## OS Permission Sync
The app re-checks iOS notification permissions whenever it becomes active and updates the backend, so device-level settings are respected even if the in-app toggle is on:
* `apps/mobile/app/_layout.tsx`
* `apps/mobile/app/notifications.tsx`
## Delivery Notes
APNs uses different endpoints for development and production. If the environment does not match the build type, APNs accepts the request but the device will not receive it. Make sure the topic matches your bundle ID (`com.launchhq.mobile`).
# Start the Backend
Source: https://docs.launchtoday.dev/quickstart
Start the API and database for Launch
This guide focuses on getting the API and database running locally. You do not
need to configure integrations to boot the backend.
## Prerequisites
If you are new to the codebase, complete the
[Prerequisites](/getting-started/prerequisites) first, then follow
[Project Setup](/getting-started/setup).
## Steps
## 1. Set Up Database
First, replace `YOUR_DB_NAME` (the value of `POSTGRES_DB`) in
`apps/api/docker-compose.yml` with a real database name (required). You can
optionally change the default user or password. Make sure the name matches
the `DATABASE_URL` you set in `.env`.
```bash theme={null}
cd apps/api
pnpm docker:up
```
Use the API shortcuts if you need them:
```bash theme={null}
pnpm docker:down
pnpm docker:restart
pnpm docker:logs
```
```bash theme={null}
# macOS
brew install postgresql
brew services start postgresql
# Create database
createdb launch
```
## 2. Configure Environment
```bash theme={null}
cd apps/api
cp example.env .env
```
For quickstart, only `DATABASE_URL` is required to boot the API. Most
integrations (auth, payments, SMS, AI, uploads) can stay unset until you enable
them in their respective guides.
When you do enable auth, set `BETTER_AUTH_SECRET` to a secure value (32+ chars).
You can generate one with:
```bash theme={null}
openssl rand -base64 32
```
If you changed the database name in Docker, update `DATABASE_URL` in `.env` to
match.
## 3. Run Migrations
```bash theme={null}
cd apps/api
pnpm db:migrate
pnpm db:generate
```
If you see a "Cannot find module '../generated/client'" error, run
`pnpm db:generate` to create the Prisma client.
## 4. Start the Backend
```bash theme={null}
cd apps/api
pnpm dev
```
You should see:
```
[INFO] Database connected successfully
[INFO] Database connection info
[INFO] API Server started on http://localhost:3001
[INFO] tRPC endpoint: http://localhost:3001/trpc
[INFO] Health check: http://localhost:3001/health
[INFO] Server Environment
Env: development
Host: localhost
```
The mobile app needs the API and database running to work end-to-end. If the
backend is not running, sign-in and data fetches will fail.
## Next Steps
Install dependencies and run the app
Understand how the codebase is organized
# Security Checklist
Source: https://docs.launchtoday.dev/security/checklist
Pre-launch security review checklist
## Pre-Launch Security Checklist
Complete this checklist before deploying to production.
## Prerequisites
* Production environment configured
* Access to logs and secrets manager
## Steps
### 🔐 Authentication
* [ ] **Strong password requirements** - Minimum 8 characters, mixed case, numbers
* [ ] **Rate limiting on auth endpoints** - Prevent brute force attacks
* [ ] **Session expiration** - Tokens expire appropriately
* [ ] **Secure token storage** - Using SecureStore, not AsyncStorage
* [ ] **OAuth state validation** - Prevent CSRF in OAuth flows
### 🔑 Secrets & Environment
* [ ] **No secrets in code** - All secrets in environment variables
* [ ] **Production secrets rotated** - Different from development
* [ ] **.env files gitignored** - Never committed to repository
* [ ] **Secrets documented** - Team knows what's needed for deployment
### 🌐 API Security
* [ ] **HTTPS only** - No HTTP in production
* [ ] **CORS configured** - Only allow trusted origins
* [ ] **Input validation** - All endpoints validate input with Zod
* [ ] **Rate limiting** - Prevent abuse on public endpoints
* [ ] **Error messages** - Don't expose internal details to users
### 📱 Mobile Security
* [ ] **Sensitive data encrypted** - Using SecureStore
* [ ] **No sensitive logs** - Remove console.log with sensitive data
* [ ] **Certificate pinning** - Consider for high-security apps
* [ ] **Jailbreak detection** - Consider for financial apps
* [ ] **App Transport Security** - Configured correctly for iOS
### 🗄️ Database
* [ ] **Parameterized queries** - Prisma handles this automatically
* [ ] **Least privilege access** - Database user has minimal permissions
* [ ] **Backups enabled** - Regular automated backups
* [ ] **Connection encrypted** - SSL for database connections
### 📦 Dependencies
* [ ] **Audit passed** - `pnpm audit` shows no critical vulnerabilities
* [ ] **Dependencies updated** - Major security patches applied
* [ ] **Lockfile committed** - pnpm-lock.yaml in version control
### 📤 File Uploads
* [ ] **File type validation** - Whitelist allowed MIME types
* [ ] **File size limits** - Prevent resource exhaustion
* [ ] **Presigned URLs expire** - Time-limited upload URLs
* [ ] **Virus scanning** - Consider for user-generated content
### 🔍 Monitoring
* [ ] **Error tracking** - Sentry or similar configured
* [ ] **Audit logging** - Security events logged
* [ ] **Alerting** - Notified of suspicious activity
## Post-Launch
After launching, maintain security by:
* **Regular dependency updates** - Weekly `pnpm audit`
* **Security monitoring** - Review error logs and alerts
* **Penetration testing** - Consider professional testing
* **Stay informed** - Follow security advisories for your stack
Security is ongoing. Schedule monthly reviews of this checklist.
## Troubleshooting
* **Checklist blocked**: identify missing env vars and fix first
* **Audit failing**: run `pnpm audit` and review vulnerabilities
## Next Steps
* [Security Overview](/security/index)
# Security
Source: https://docs.launchtoday.dev/security/index
Security best practices for your production app
## Overview
Security is critical for any production app handling user data. This section covers essential security practices for Launch applications.
## Prerequisites
* Production environment configured
* Access to deployment logs and secrets manager
Secure storage, data protection, and device security
Pre-launch security review checklist
## Built-in Security Features
Launch includes these security features out of the box:
| Feature | Description |
| ------------------------ | --------------------------------------- |
| **Presigned URLs** | S3 uploads without exposing credentials |
| **Secure Token Storage** | Expo SecureStore for sensitive data |
| **HTTPS Only** | All API communication over TLS |
| **Input Validation** | Zod schemas for all API inputs |
| **CORS Configuration** | Restricted origins for API access |
| **Session Management** | Secure session tokens with Better Auth |
## Security Principles
Always validate and sanitize all data coming from the client, even if it
seems to come from your own app.
Only request permissions you need. Only expose data the user should access.
Multiple layers of security. If one fails, others still protect.
Never commit secrets to git. Use environment variables and secret managers.
## Steps
## Quick Security Wins
Ensure your production API uses HTTPS only
Add security headers to your API responses
Prevent brute force attacks on login
Use Zod schemas for every API endpoint
Run `pnpm audit` regularly
**Before launching**, complete the [Security Checklist](/security/checklist)
to ensure you haven't missed any critical security measures.
## Troubleshooting
* **Leaky logs**: review logging config and sanitize inputs
* **Auth failures**: verify provider env vars and callback URLs
## Next Steps
* [Security Checklist](/security/checklist)
* [Mobile Security](/security/mobile-security)
# Jailbroken Devices
Source: https://docs.launchtoday.dev/security/jailbroken-devices
Why we detect compromised devices and how to use the guard safely
## Overview
Jailbroken (iOS) or rooted (Android) devices bypass platform security
protections. This makes it easier for malware or tampering tools to intercept
requests, read local data, or manipulate app behavior. Launch includes optional
device integrity checks to help you protect sensitive workflows without blocking
legitimate users from signing in.
## What “jailbroken” means
On a jailbroken or rooted device, users (or malware) can:
* Inject code into running apps.
* Read or modify local storage that should be private.
* Bypass app sandboxing and OS restrictions.
* Intercept network traffic or tamper with requests.
That doesn’t mean every jailbroken device is malicious, but it does raise the
risk profile for sensitive operations.
## What Launch uses for detection
Launch uses the `jail-monkey` library to detect compromised devices and runtime
tampering. We expose two guards:
* `DeviceIntegrityGuard` (root/jailbreak detection)
* `RuntimeIntegrityGuard` (debug/hook detection)
These are deliberately opt-in so you can decide where to enforce them.
## Why we have it
Most apps don’t need to block everything on a compromised device. The common
pattern is to **allow login** but **protect sensitive actions** like payments,
subscription changes, or account deletion.
This approach keeps support accessible for legitimate users while still
protecting high-risk workflows.
## Recommended usage in production
Apply guards to screens that:
* Modify billing or entitlements (payments, subscriptions).
* Perform irreversible actions (delete account).
* Access or upload sensitive data (file uploads, KYC, documents).
* Expose private user data (settings, profile, chat history).
Avoid guarding low-risk screens such as onboarding, login, or general feature
discovery. If a device is compromised, it is better to let the user sign in and
reach support than to block the entire app.
## Example placement
* Guarded: payments, subscription, delete account, file uploads, settings.
* Not guarded: auth, notifications, general feature list.
## Tips for extending safely
* Start with the defaults, then expand as you add sensitive workflows.
* If you build identity verification or compliance flows, add guards there.
* Consider using a “soft block” (show a warning) instead of a hard block for
some screens.
* Always log guard triggers so you can support false positives.
## FAQs
**Does this block TestFlight users?**\
It shouldn’t, but build and debug checks can produce false positives if used
incorrectly. In Launch, the runtime check is async-safe and doesn’t treat a
Promise as a boolean.
**Is this required to ship?**\
No. It’s optional and intended as a security layer you can enable where needed.
**Can I turn it off for development?**\
Yes. The guards allow dev bypass by default to avoid blocking local testing.
# Mobile Security
Source: https://docs.launchtoday.dev/security/mobile-security
Secure storage, data protection, and mobile security best practices
## Secure by Default
Launch is configured with security best practices out of the box. Here's what's protected and how.
## Prerequisites
* Expo SecureStore installed
* Auth client configured
## Steps
### Authentication & Token Storage
**Auth tokens are stored securely** using `expo-secure-store`
```typescript theme={null}
// lib/auth/client.ts
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
plugins: [
expoClient({
scheme: "launch",
storagePrefix: "launch",
storage: SecureStore, // ✅ Secure storage
}),
],
});
```
This means:
* **iOS**: Tokens are stored in the Keychain (hardware-backed encryption)
* **Android**: Tokens are stored in the Keystore (hardware-backed encryption)
Even on jailbroken/rooted devices, this data is significantly harder to extract than plaintext storage.
***
## Choosing the Right Storage
Both `AsyncStorage` and `SecureStore` have their place. The key is knowing what data belongs where.
Unencrypted key-value storage. Fast and simple, but readable on compromised
devices.
Encrypted via native Keychain/Keystore. Hardware-backed protection for
sensitive data.
### When to Use AsyncStorage ✅
AsyncStorage is perfectly fine for **non-sensitive, non-PII data**:
| Use Case | Example | Why It's OK |
| ----------------- | -------------------------- | -------------------------- |
| Theme preferences | `"dark"` or `"light"` | No user impact if exposed |
| App settings | `{ notifications: true }` | Non-personal configuration |
| Onboarding state | `{ hasSeenIntro: true }` | No security implications |
| Cache data | Recently viewed items | Improves UX, not sensitive |
| Feature flags | `{ betaFeatures: true }` | Non-sensitive app state |
| Upload queue | Progress tracking metadata | No credentials involved |
```typescript theme={null}
// ✅ These are fine in AsyncStorage
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("@theme", "dark");
await AsyncStorage.setItem("@onboarding_complete", "true");
await AsyncStorage.setItem("@last_viewed_category", "electronics");
```
### When to Use SecureStore 🔒
Use SecureStore for anything that could harm the user if exposed:
| Use Case | Example | Why It Needs Protection |
| -------------- | ----------------------- | ------------------------- |
| Auth tokens | JWT, session tokens | Enables account access |
| Passwords | User credentials | Direct account compromise |
| API keys | Third-party secrets | Service abuse |
| Payment data | Card numbers, CVV | Financial fraud |
| PII | SSN, passport, DOB | Identity theft |
| Biometric data | Face/fingerprint hashes | Privacy violation |
```typescript theme={null}
// ✅ Use SecureStore for sensitive data
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
await SecureStore.setItemAsync("refreshToken", refreshToken);
```
### What Launch Stores Where
| Data | Storage | Why |
| ----------------------- | --------------- | ---------------------------------- |
| Auth tokens | `SecureStore` ✅ | Sensitive - enables account access |
| Session data | `SecureStore` ✅ | Sensitive - authentication state |
| Color scheme preference | `AsyncStorage` | Non-sensitive UI preference |
| Upload queue state | `AsyncStorage` | Non-sensitive progress tracking |
### Quick Decision Guide
Ask yourself: **"If someone read this data, could they harm the user?"**
* **No** → AsyncStorage is fine
* **Yes** → Use SecureStore
**Never store in AsyncStorage:** - Authentication tokens or session IDs -
Passwords, PINs, or security codes - API keys or secrets - Credit card numbers
or financial data - Personal identification (SSN, passport, driver's license)
* Health or medical information
## Troubleshooting
* **Tokens not stored**: check SecureStore permissions
* **Unexpected logout**: verify session persistence and SecureStore availability
## Next Steps
* [Security Checklist](/security/checklist)
***
## Best Practices
### Be Mindful About What You Store
Before storing any data locally, consider its sensitivity:
```typescript theme={null}
// ✅ Non-sensitive data → AsyncStorage
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("@theme", "dark");
await AsyncStorage.setItem("@language", "en");
await AsyncStorage.setItem("@has_rated_app", "true");
// ✅ Sensitive data → SecureStore
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
await SecureStore.setItemAsync("apiKey", apiKey);
```
### Think Before You Store
When adding features that persist data, ask:
1. **Could this data harm the user if exposed?** → SecureStore
2. **Could this data impersonate the user?** → SecureStore
3. **Is this financial or personal information?** → SecureStore
4. **Is this just a preference or app state?** → AsyncStorage is fine
### Audit Storage Periodically
Search your codebase to review what's being stored:
```bash theme={null}
# Find all AsyncStorage usage
grep -r "AsyncStorage" --include="*.tsx" --include="*.ts" apps/mobile/
# Find all SecureStore usage
grep -r "SecureStore" --include="*.tsx" --include="*.ts" apps/mobile/
```
Ensure sensitive data isn't accidentally stored in AsyncStorage.
***
## What Happens on Jailbroken/Rooted Devices
### AsyncStorage Exposure
On a jailbroken iOS device or rooted Android device:
```bash theme={null}
# Attacker can simply read:
/var/mobile/Containers/Data/Application/{APP_ID}/Documents/AsyncStorage/
# Contents are plain JSON:
{"@color_scheme_preference": "dark"} # ← This is fine
{"@auth_token": "eyJhbGc..."} # ← This would be BAD
```
### SecureStore Protection
SecureStore data is stored in:
* **iOS**: Keychain (encrypted, access-controlled)
* **Android**: Keystore (hardware-backed encryption)
Even with root access, extracting Keychain data requires:
* Specialized forensic tools
* Device-specific exploits
* Significantly more effort and expertise
SecureStore isn't impenetrable on jailbroken devices, but it raises the bar
dramatically compared to AsyncStorage.
***
## Additional Mobile Security Measures
### Remove Sensitive Console Logs
Before production builds, remove any console.log statements that output sensitive data:
```typescript theme={null}
// ❌ Remove before production
console.log("User token:", authToken);
console.log("API response:", response);
// ✅ Use proper logging with levels
if (__DEV__) {
console.log("Debug info:", safeData);
}
```
### Consider Certificate Pinning (High-Security Apps)
For apps handling financial data or highly sensitive information:
```typescript theme={null}
// Using react-native-ssl-pinning or similar
import { fetch } from "react-native-ssl-pinning";
const response = await fetch(url, {
sslPinning: {
certs: ["cert1", "cert2"],
},
});
```
### Consider Jailbreak Detection (Financial Apps)
For banking or payment apps, consider detecting compromised devices:
```typescript theme={null}
import JailMonkey from "jail-monkey";
if (JailMonkey.isJailBroken()) {
// Warn user or restrict functionality
}
```
Jailbreak detection can be bypassed by determined attackers. Use it as one
layer in defense-in-depth, not as your only protection.
***
## Quick Security Audit
Run this checklist for your app:
Find all uses: `grep -r "AsyncStorage" apps/mobile/`
Check each usage stores only non-sensitive data
Verify `lib/auth/client.ts` uses `storage: SecureStore`
Search for `console.log` with tokens, passwords, or user data
Verify payment SDKs (Stripe, RevenueCat) use secure storage
***
## Summary
| Aspect | Launch Default | Status |
| ------------------ | ----------------------------------- | -------------------- |
| Auth token storage | `expo-secure-store` | ✅ Secure |
| Session management | Better Auth + SecureStore | ✅ Secure |
| User preferences | `AsyncStorage` | ✅ OK (non-sensitive) |
| Upload progress | `AsyncStorage` | ✅ OK (non-sensitive) |
| API credentials | Environment variables (server-side) | ✅ Secure |
**Launch is secure by default.** Authentication tokens and sensitive session
data are stored using `expo-secure-store`, which provides hardware-backed
encryption on both iOS and Android.
# Troubleshooting
Source: https://docs.launchtoday.dev/troubleshooting/index
Common issues and how to fix them
## Prerequisites
* App and API running locally
* Access to logs
## Steps
1. Identify the failing flow
2. Apply the relevant fix below
3. Re-run the flow and confirm
## Common Issues
Quick fixes for the most common problems you'll encounter.
## Quick Fixes
### Metro Bundler Issues
```bash theme={null}
# Clear cache and restart
cd apps/mobile
pnpm start --clear
# Or nuclear option
rm -rf node_modules/.cache
watchman watch-del-all
pnpm start --clear
```
### iOS Build Failures
```bash theme={null}
cd apps/mobile/ios
rm -rf Pods Podfile.lock
pod install --repo-update
cd ..
pnpm ios
```
### Android Build Failures
```bash theme={null}
cd apps/mobile/android
./gradlew clean
cd ..
pnpm android
```
### Prisma Issues
```bash theme={null}
cd apps/api
pnpm prisma generate
pnpm prisma migrate dev
```
## Error Reference
**Cause**: Missing dependency or incorrect import
**Fix**:
```bash theme={null}
pnpm install
pnpm start --clear
```
**Cause**: API not reachable from device **Fix**: 1. Check API is running
(`pnpm dev` in apps/api) 2. Update API URL to your local IP (not localhost)
3\. Check CORS settings include your device's origin
**Cause**: Misconfigured Apple credentials **Fix**: 1. Verify bundle ID
matches APPLE\_CLIENT\_ID 2. Check APPLE\_PRIVATE\_KEY is correctly formatted 3.
Ensure Sign In with Apple is enabled in Apple Developer Console
**Cause**: OAuth started on one origin but the callback returned to another (common when mixing LAN URLs with ngrok).
**Fix**:
* If API uses ngrok: set both `BETTER_AUTH_URL` (API) and `EXPO_PUBLIC_API_URL` (mobile) to the same ngrok HTTPS origin
* Ensure Google Console **Authorized redirect URI** matches exactly: `https://YOUR_NGROK.ngrok-free.app/api/auth/callback/google`
**Cause**: PostgreSQL not running or wrong credentials **Fix**: 1. Start
PostgreSQL: `docker-compose up -d` or `brew services start postgresql` 2.
Verify DATABASE\_URL in .env 3. Run migrations: `pnpm prisma migrate dev`
**Cause**: React version mismatch or incorrect hook usage **Fix**: 1. Ensure
only one React version: `pnpm why react` 2. Check hooks are only called at
top level of components
## Next Steps
* [Incident Debugging](/essentials/incident-debugging)
## Getting Help
If you're still stuck:
1. **Search existing issues** on GitHub
2. **Check Expo documentation** at [docs.expo.dev](https://docs.expo.dev)
3. **Ask in the community** - Discord, GitHub Discussions
4. **Open an issue** with reproduction steps
When reporting issues, include: - Error message (full stack trace) - Steps to
reproduce - Platform (iOS/Android) and version - Expo SDK version