Mobile apps require special handling for authentication flows. When a user clicks a magic link email or completes OAuth signin, the browser needs to redirect back to your native app—not a website. For self-hosted Supabase deployments, this means configuring deep linking correctly on both your server and mobile app.
This guide covers custom URL schemes, universal links, and the specific configuration needed to make mobile auth work reliably with self-hosted Supabase.
Why Deep Linking Matters for Mobile Auth
Authentication flows often redirect users through external pages: email confirmation links, OAuth provider consent screens, password reset pages. On the web, these redirects land on your callback URL and everything works. On mobile, the browser doesn't know how to hand control back to your app without deep linking.
Two approaches exist:
Custom URL Schemes (myapp://callback): Simple to configure, work everywhere, but can be hijacked by malicious apps since any app can register any scheme.
Universal Links (iOS) / App Links (Android): Secure HTTPS-based links that require domain verification. The OS confirms your app owns the domain before opening it. More complex to set up, but the recommended approach for production.
For self-hosted Supabase, both approaches require server-side configuration that differs from Supabase Cloud.
Prerequisites
Before configuring deep linking, ensure you have:
- A working self-hosted Supabase deployment
- A domain with HTTPS configured (via reverse proxy or custom domain)
- Your mobile app project (Flutter, React Native, or native iOS/Android)
- Access to your domain's web server for hosting verification files
Configuring Supabase for Mobile Redirects
Environment Variables
Add your mobile app's redirect URLs to the allow list in your .env file:
# Site URL - your primary web domain GOTRUE_SITE_URL=https://yourdomain.com # Redirect URLs - include both web and mobile schemes GOTRUE_URI_ALLOW_LIST=https://yourdomain.com/*,myapp://callback,myapp.staging://callback
For production apps using universal links:
GOTRUE_URI_ALLOW_LIST=https://yourdomain.com/*,https://yourdomain.com/auth/callback
The key difference from Supabase Cloud: you configure these in environment variables, not a dashboard. With tools like Supascale, you can manage these through a UI instead.
Docker Compose Configuration
Ensure your auth service receives these variables:
services:
auth:
environment:
GOTRUE_SITE_URL: ${GOTRUE_SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${GOTRUE_URI_ALLOW_LIST}
GOTRUE_EXTERNAL_EMAIL_ENABLED: true
Restart the auth service after changes:
docker compose restart auth
Custom URL Schemes (Simple Approach)
Custom URL schemes work without domain verification and are useful for development and testing.
React Native / Expo Configuration
In your app.json or app.config.js:
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.company.myapp"
},
"android": {
"package": "com.company.myapp"
}
}
}
Install the required dependencies:
npx expo install expo-linking expo-web-browser @supabase/supabase-js
Flutter Configuration
In android/app/src/main/AndroidManifest.xml:
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="myapp" android:host="callback" /> </intent-filter>
In ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Implementing Auth with Custom Schemes
React Native example with OAuth:
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-self-hosted-supabase.com',
'your-anon-key'
);
async function signInWithGoogle() {
const redirectUrl = Linking.createURL('callback');
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: redirectUrl,
skipBrowserRedirect: true,
},
});
if (data?.url) {
const result = await WebBrowser.openAuthSessionAsync(
data.url,
redirectUrl
);
if (result.type === 'success') {
const { url } = result;
// Extract session from URL and set it
await handleAuthCallback(url);
}
}
}
Universal Links and App Links (Production Approach)
For production apps, use HTTPS-based links that the OS verifies against your domain.
Hosting Verification Files
Both iOS and Android require files hosted at specific paths on your domain.
iOS (apple-app-site-association):
Create a file at https://yourdomain.com/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.company.myapp",
"paths": ["/auth/callback", "/auth/*"]
}
]
}
}
Replace TEAMID with your Apple Developer Team ID.
Android (assetlinks.json):
Create a file at https://yourdomain.com/.well-known/assetlinks.json:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.company.myapp",
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
}
}]
Get your fingerprint with:
keytool -list -v -keystore your-release-key.keystore
Serving Verification Files with Nginx
If using Nginx as your reverse proxy, add these location blocks:
location /.well-known/apple-app-site-association {
default_type application/json;
alias /var/www/html/.well-known/apple-app-site-association;
}
location /.well-known/assetlinks.json {
default_type application/json;
alias /var/www/html/.well-known/assetlinks.json;
}
Headers matter. iOS requires Content-Type: application/json without a .json extension in the filename.
iOS Entitlements
In your Xcode project, add the Associated Domains capability:
applinks:yourdomain.com
In your Runner.entitlements file:
<key>com.apple.developer.associated-domains</key> <array> <string>applinks:yourdomain.com</string> </array>
Android Intent Filters
In AndroidManifest.xml, add autoVerify for automatic verification:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/auth/callback" />
</intent-filter>
Using PKCE for Mobile Auth
PKCE (Proof Key for Code Exchange) solves a critical problem: some email clients strip URL fragments. When Supabase sends tokens in the fragment (#access_token=...), they disappear before reaching your app.
Enable PKCE in your auth requests:
const { data, error } = await supabase.auth.signInWithOtp({
email: '[email protected]',
options: {
emailRedirectTo: 'https://yourdomain.com/auth/callback',
// PKCE sends tokens as query params, not fragments
},
});
For self-hosted Supabase, ensure PKCE is enabled:
GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED: true GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10
The server-side callback then exchanges the code for tokens:
// In your callback handler
const code = new URL(url).searchParams.get('code');
if (code) {
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
}
Troubleshooting Common Issues
Links Open in Browser Instead of App
Symptom: Clicking auth links opens Safari/Chrome instead of your app.
Causes and fixes:
Verification files not accessible: Test with
curl -I https://yourdomain.com/.well-known/apple-app-site-association. Must return 200 with correct content type.Wrong team ID or package name: Double-check against your Apple Developer account or Android signing certificate.
App not installed via TestFlight/Play Store: Universal links require a signed release build. Development builds may not work.
iOS Simulator limitation: Universal links don't work in the iOS Simulator. Use a physical device.
Redirect Goes to Kong Internal URL
Symptom: Auth emails contain http://kong/auth/v1/verify?token=... instead of your domain.
Fix: Set API_EXTERNAL_URL in your environment:
API_EXTERNAL_URL=https://yourdomain.com GOTRUE_SITE_URL=https://yourdomain.com
This is a common self-hosted issue where internal Docker service names leak into external URLs.
Token Missing After Redirect
Symptom: The callback URL arrives but access_token is missing.
Causes:
Email client stripped fragment: Gmail on some Android versions removes URL fragments. Use PKCE to send tokens as query parameters instead.
Wrong URL parsing: Fragments (
#) are client-side only. If using server-side rendering, you won't see them. Use PKCE or handle parsing client-side.
Session Not Persisting
Symptom: User appears logged in momentarily, then gets logged out.
Fix: Ensure proper storage configuration:
import AsyncStorage from '@react-native-async-storage/async-storage';
const supabase = createClient(
'https://your-self-hosted-supabase.com',
'your-anon-key',
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false, // Important for mobile
},
}
);
Testing Your Configuration
Verify Supabase Configuration
Check that your redirect URLs are properly registered:
curl -X GET 'https://yourdomain.com/auth/v1/settings' \ -H "apikey: YOUR_ANON_KEY" | jq '.external'
Test Deep Link Handling
On iOS, test from Terminal:
xcrun simctl openurl booted "myapp://callback?code=test"
On Android:
adb shell am start -W -a android.intent.action.VIEW \ -d "myapp://callback?code=test" com.company.myapp
Validate Universal Links
Apple provides a validator at https://search.developer.apple.com/appsearch-validation-tool/
For Android, use:
adb shell pm get-app-links com.company.myapp
Managing Deep Links with Supascale
When managing multiple self-hosted Supabase projects, deep linking configuration becomes repetitive. Supascale simplifies this by:
- Managing redirect URL allow lists through a dashboard
- Handling custom domain configuration that universal links depend on
- Providing automatic SSL certificates required for HTTPS verification files
- Backing up auth configuration alongside database backups
The one-time $39.99 license covers unlimited projects, making it practical to maintain separate configurations for development, staging, and production apps.
Conclusion
Deep linking for self-hosted Supabase requires configuration at three levels: your Supabase environment variables, your mobile app project, and (for universal links) your web server. The key differences from Supabase Cloud:
- Environment variables replace dashboard settings for redirect URLs
- Verification files must be hosted on your domain, not Supabase's
- Internal URLs like
kongcan leak into external links without properAPI_EXTERNAL_URLconfiguration
Start with custom URL schemes for development, then migrate to universal links for production. Use PKCE to avoid fragment-stripping issues in email clients. Test on real devices—simulators have limitations with deep links.
With proper configuration, mobile auth on self-hosted Supabase works as reliably as the managed service, with full control over your authentication infrastructure.
