Deep Linking in Flutter:
The Complete Breakdown
Flutter does not handle deep links automatically. Here is the complete setup for getting users from a confirmation email back into your app, including the parts that break without any error to trace.
We were building a session booking app. The auth flow looked straightforward: user signs up, gets a confirmation email, clicks the link, lands inside the app ready to book. In practice, clicking the link opened a browser, the user saw a generic redirect page, the session token was gone, and they had no idea what to do next.
The problem was not a bug in our code. Flutter gives you the UI layer and nothing else. Deep linking requires platform configuration on both iOS and Android, a small piece of web infrastructure on your domain, and Dart code to handle the incoming URL. Most tutorials cover one or two of these pieces. This post covers all of them.
Two types of links, one right choice
There are two kinds of links that can open a mobile app, and they behave very differently.
URI schemes like myapp://auth/callback work between apps on the same device but email clients do not trust them. They are useful for local development and nothing else.
Universal Links on iOS and App Links on Android are real https:// URLs on a domain you control. The OS checks whether an app is registered for that domain and opens it directly. If the app is not installed, the browser handles the URL normally. These are what email clients send, and what Supabase auth emails use.
The setup requires three layers to work together: platform configuration in the app, a verification file on your domain, and a redirect page that completes the flow when the direct link cannot fire.
Configure Supabase and enable PKCE
In the Supabase dashboard under Authentication, set your redirect URL to a real domain you control. Keep the custom scheme as a secondary option for local development only.
Initialize Supabase with the PKCE auth flow. Without PKCE, tokens arrive in the URL fragment after a # character. Gmail on Android strips URL fragments before opening links. PKCE puts the auth code in a query parameter instead, which survives every email client we have tested.
Android App Links setup
Intent filter
Add an intent filter inside the MainActivity activity block in AndroidManifest.xml. The android:autoVerify=”true” attribute tells Android to verify your domain ownership so it opens the app directly without showing a disambiguation dialog.
Digital Asset Links file
Android will not open the app from https:// links without this verification file hosted at your domain. If the file is missing or incorrect, Android falls back to the browser with no error shown anywhere.
iOS Universal Links setup
Entitlements
In Xcode, select your target, go to Signing and Capabilities, and add the Associated Domains capability. Register your domain with the applinks: prefix:
Apple App Site Association
Host this JSON file at your domain. Note there is no .json extension in the filename. Apple fetches this file when the app is installed, not at runtime, so any changes you make take effect on the next install.
The redirect page nobody mentions
When the user clicks the email link, the OS fires the Universal or App Link and opens the app directly if it is installed and the domain has been verified. If not, the browser opens instead.
You need a small HTML page at /auth/callback that reads the incoming parameters and redirects to the app via its URI scheme. This is the piece most tutorials skip, and without it the flow breaks the moment a user clicks the link on a device where verification has not completed yet.
The complete flow with this page in place:
- 01User clicks link in confirmation email
- 02OS checks if an app is registered for yourdomain.com
- YApp opens directly with the URI and code parameter
- NBrowser opens /auth/callback?code=…
- HTML page redirects to com.yourapp://auth/callback?code=…
- OS opens app via URI scheme
- 03Flutter app_links catches the incoming URI
- 04Supabase.auth.getSessionFromUrl(uri) stores the session
- 05Auth state changes, router navigates to home screen
Handling the link in Flutter
Add app_links to your pubspec.yaml dependencies. Starting from app_links v6, uriLinkStream covers both cold start and warm start cases, so you only need one listener. Call it as early as possible in your app lifecycle, before any navigation happens.
Connect the router to Supabase auth state so navigation reacts automatically when a session is established. The example below uses go_router with a refresh stream wrapper:
The things that break silently
These are the failures we hit during testing. None of them produce a useful error message.
After this setup the flow worked cleanly across iOS and Android, cold start and warm start, Gmail, Apple Mail, and Outlook. The testers who had been landing on blank redirect pages were opening the app directly to the booking screen, authenticated and ready to go.
None of the individual pieces are complicated. The challenge is that they all have to be right at the same time, and when something is wrong the failure is usually silent.


