Deep Linking for Self-Hosted Supabase: Mobile App Authentication Guide

Configure deep linking and auth redirects for iOS and Android apps with self-hosted Supabase. Custom URL schemes, universal links, and PKCE setup.

Cover Image for Deep Linking for Self-Hosted Supabase: Mobile App Authentication Guide

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:

  1. A working self-hosted Supabase deployment
  2. A domain with HTTPS configured (via reverse proxy or custom domain)
  3. Your mobile app project (Flutter, React Native, or native iOS/Android)
  4. 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);
    }
  }
}

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

Symptom: Clicking auth links opens Safari/Chrome instead of your app.

Causes and fixes:

  1. Verification files not accessible: Test with curl -I https://yourdomain.com/.well-known/apple-app-site-association. Must return 200 with correct content type.

  2. Wrong team ID or package name: Double-check against your Apple Developer account or Android signing certificate.

  3. App not installed via TestFlight/Play Store: Universal links require a signed release build. Development builds may not work.

  4. 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:

  1. Email client stripped fragment: Gmail on some Android versions removes URL fragments. Use PKCE to send tokens as query parameters instead.

  2. 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'

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

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

When managing multiple self-hosted Supabase projects, deep linking configuration becomes repetitive. Supascale simplifies this by:

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:

  1. Environment variables replace dashboard settings for redirect URLs
  2. Verification files must be hosted on your domain, not Supabase's
  3. Internal URLs like kong can leak into external links without proper API_EXTERNAL_URL configuration

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.


Further Reading