Skip to main content
Mobile Launcher
Security

Security

Security best practices for your mobile application

Security

MobileLauncher Standard comes with security best practices built in. This guide covers what's already configured, what you need to be aware of, and how to keep your app secure.

Quick Overview

The boilerplate handles the most critical security concerns out of the box:

AreaWhat's Built In
Token storageAuth tokens stored in expo-secure-store (encrypted keychain on iOS, encrypted shared preferences on Android)
Environment variables.env files ignored in .gitignore, EXPO_PUBLIC_ prefix convention enforced
Input validationZod schemas validate all form inputs before sending to the server
HTTPSAll API calls configured to use HTTPS only
Type safetyTypeScript throughout, catches data handling bugs at compile time

Environment Variables

Your app uses .env files to manage configuration. Understanding the distinction between public and secret variables is critical.

The EXPO_PUBLIC_ Rule

Any variable prefixed with EXPO_PUBLIC_ is embedded in your JavaScript bundle and is visible to anyone who decompiles your app.

# SAFE, These are designed to be public (client-side keys) EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs... # NEVER DO THIS, Secret keys must stay on your backend EXPO_PUBLIC_STRIPE_SECRET_KEY=sk_live_... # WRONG EXPO_PUBLIC_OPENAI_API_KEY=sk-... # WRONG EXPO_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJ... # WRONG

Supabase Anon Key is safe to expose, it's designed for client-side use and is restricted by Row Level Security (RLS) policies. The Service Role Key bypasses RLS and must never be in client code.

Best Practices

  1. Never commit .env files, Already ignored in .gitignore. Use .env.example with placeholder values for documentation.
  2. Use EAS Secrets for CI/CD, Store production keys as EAS Secrets, not in your repo.
  3. Rotate compromised keys immediately, If a secret is accidentally committed, consider it compromised and regenerate it.

Authentication Security

The boilerplate uses Supabase Auth with several security layers.

Token Management

  • Access tokens are stored in expo-secure-store (encrypted native storage)
  • Auto-refresh handles token expiration transparently
  • Logout clears all stored tokens and session data

Secure Storage vs Regular Storage

StorageUse ForSecurity Level
expo-secure-storeAuth tokens, sensitive user dataEncrypted (iOS Keychain / Android Keystore)
MMKV (Redux Persist)App state, preferences, cached dataNot encrypted, fast but not for secrets
AsyncStorageNot used in this boilerplateUnencrypted, avoid for sensitive data

Rule of thumb: If the data could be used to impersonate a user or access their account, it belongs in SecureStore. Everything else can go in MMKV.

Social Auth (Google & Apple Sign-In)

  • Google Sign-In uses OAuth 2.0 with PKCE flow, tokens are never exposed to JavaScript
  • Apple Sign-In uses Apple's native authentication, credentials are handled by the OS
  • Both providers issue tokens that are exchanged server-side through Supabase

API Security

HTTPS Only

All API communication must use HTTPS. The boilerplate enforces this:

  • Supabase URLs are always https://
  • Firebase endpoints use Google's TLS infrastructure
  • RevenueCat SDK communicates over HTTPS by default

Row Level Security (RLS)

Supabase uses PostgreSQL's Row Level Security to ensure users can only access their own data. The boilerplate's database setup includes RLS policies:

Sql
-- Example: Users can only read their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);

-- Example: Users can only update their own data
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);

Always enable RLS on every table. A table without RLS policies is publicly readable/writable by anyone with your Supabase URL and anon key.

Input Validation

The boilerplate validates data at two levels:

  1. Client-side, Zod schemas validate form inputs before sending (better UX, prevents obvious errors)
  2. Server-side, Supabase column constraints and RLS policies enforce data integrity
Typescript
// Example: Login form validation with Zod
const loginSchema = z.object({
  email: z.string().email("Invalid email address"),
  password: z.string().min(8, "Password must be at least 8 characters"),
});

Permissions

Only request device permissions your app actually needs. Unnecessary permissions trigger stricter app store reviews and erode user trust.

Audit Your Permissions

Check your app.json for both iOS and Android:

Json
// iOS, Only include infoPlist entries you use
"infoPlist": {
  "NSCameraUsageDescription": "Take profile photos",
  "NSPhotoLibraryUsageDescription": "Choose a profile picture"
}

// Android, Only include permissions you use
"permissions": [
  "CAMERA",
  "READ_EXTERNAL_STORAGE"
]

Remove any permissions you're not using. Common offenders:

PermissionRemove If...
NSLocationWhenInUseUsageDescriptionYour app doesn't use location
NSMicrophoneUsageDescriptionYour app doesn't record audio
RECORD_AUDIOYour app doesn't record audio
ACCESS_FINE_LOCATIONYour app doesn't need precise location

Dependency Security

Third-party packages can introduce vulnerabilities. Keep your dependencies up to date.

Regular Audits

# Check for known vulnerabilities yarn audit # Update packages to latest compatible versions yarn upgrade-interactive

What the Boilerplate Pins

Critical dependencies are pinned to tested versions to avoid breaking changes:

  • Expo SDK, Major version pinned (e.g., SDK 54)
  • React Native, Matches Expo's tested version
  • Supabase JS, Pinned to stable release

When upgrading Expo SDK versions, follow the Expo upgrade guide carefully. SDK upgrades often require coordinated dependency updates.


Production Security Checklist

Go through this checklist before every production release:

CategoryCheckStatus
Environment.env is in .gitignore and not committedRequired
SecretsNo backend secrets in EXPO_PUBLIC_ variablesRequired
StorageAuth tokens stored in SecureStore, not MMKV or AsyncStorageRequired
TransportAll API calls use HTTPSRequired
DatabaseRLS enabled on every Supabase tableRequired
PermissionsOnly necessary device permissions requestedRequired
Dependenciesyarn audit shows no critical vulnerabilitiesRecommended
Error reportingSentry configured, no sensitive data in error payloadsRecommended
AnalyticsFirebase Analytics does not log PII (emails, names) in custom eventsRecommended
Deep linksURL schemes validated and don't expose sensitive routesRecommended

Common Security Mistakes

MistakeWhy It's DangerousFix
Putting secret keys in EXPO_PUBLIC_Anyone can decompile your app and extract themMove secrets to your backend or Supabase Edge Functions
Disabling RLS "to test" and forgetting to re-enableYour entire database becomes publicly accessibleAlways develop with RLS enabled
Storing auth tokens in MMKV or AsyncStorageUnencrypted storage, accessible on rooted/jailbroken devicesUse expo-secure-store
Committing .env to gitAPI keys visible in repo history foreverUse git filter-branch or BFG to remove, then rotate all keys
Logging sensitive data in developmentLogs can persist on device or in crash reportsUse conditional logging that strips PII in production
Not validating deep link parametersCan lead to unauthorized actions or navigation hijackingValidate all deep link params before processing