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:
| Area | What's Built In |
|---|---|
| Token storage | Auth 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 validation | Zod schemas validate all form inputs before sending to the server |
| HTTPS | All API calls configured to use HTTPS only |
| Type safety | TypeScript 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... # WRONGSupabase 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
- Never commit
.envfiles, Already ignored in.gitignore. Use.env.examplewith placeholder values for documentation. - Use EAS Secrets for CI/CD, Store production keys as EAS Secrets, not in your repo.
- 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
| Storage | Use For | Security Level |
|---|---|---|
expo-secure-store | Auth tokens, sensitive user data | Encrypted (iOS Keychain / Android Keystore) |
MMKV (Redux Persist) | App state, preferences, cached data | Not encrypted, fast but not for secrets |
AsyncStorage | Not used in this boilerplate | Unencrypted, 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:
-- 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:
- Client-side, Zod schemas validate form inputs before sending (better UX, prevents obvious errors)
- Server-side, Supabase column constraints and RLS policies enforce data integrity
// 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:
// 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:
| Permission | Remove If... |
|---|---|
NSLocationWhenInUseUsageDescription | Your app doesn't use location |
NSMicrophoneUsageDescription | Your app doesn't record audio |
RECORD_AUDIO | Your app doesn't record audio |
ACCESS_FINE_LOCATION | Your 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-interactiveWhat 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:
| Category | Check | Status |
|---|---|---|
| Environment | .env is in .gitignore and not committed | Required |
| Secrets | No backend secrets in EXPO_PUBLIC_ variables | Required |
| Storage | Auth tokens stored in SecureStore, not MMKV or AsyncStorage | Required |
| Transport | All API calls use HTTPS | Required |
| Database | RLS enabled on every Supabase table | Required |
| Permissions | Only necessary device permissions requested | Required |
| Dependencies | yarn audit shows no critical vulnerabilities | Recommended |
| Error reporting | Sentry configured, no sensitive data in error payloads | Recommended |
| Analytics | Firebase Analytics does not log PII (emails, names) in custom events | Recommended |
| Deep links | URL schemes validated and don't expose sensitive routes | Recommended |
Common Security Mistakes
| Mistake | Why It's Dangerous | Fix |
|---|---|---|
Putting secret keys in EXPO_PUBLIC_ | Anyone can decompile your app and extract them | Move secrets to your backend or Supabase Edge Functions |
| Disabling RLS "to test" and forgetting to re-enable | Your entire database becomes publicly accessible | Always develop with RLS enabled |
| Storing auth tokens in MMKV or AsyncStorage | Unencrypted storage, accessible on rooted/jailbroken devices | Use expo-secure-store |
Committing .env to git | API keys visible in repo history forever | Use git filter-branch or BFG to remove, then rotate all keys |
| Logging sensitive data in development | Logs can persist on device or in crash reports | Use conditional logging that strips PII in production |
| Not validating deep link parameters | Can lead to unauthorized actions or navigation hijacking | Validate all deep link params before processing |