Deep Linking in Flutter:
The Complete Breakdown
Deep linking in Flutter does not work out of the box. 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. We later wrote up the full project in our booking app development case study, but the deep link handling was the part that nearly derailed the launch.
The problem was not a bug in our code. Flutter gives you the UI layer and nothing else. Deep linking in Flutter 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, and the official Flutter deep linking documentation is the reference for anything we do not repeat here.
Deep linking in Flutter: 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 for Flutter
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 for Flutter
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 most tutorials skip
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:
- ●User clicks link in confirmation email
- ●OS 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
- ●Flutter app_links catches the incoming URI
- ●Supabase.auth.getSessionFromUrl(uri) stores the session
- ●Auth state changes, router navigates to home screen
Handling deep linking in Flutter code
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 same handler covers links beyond login. Once auth works, product links such as a shared booking, an invitation to a team or a password reset follow the same path: the domain opens the app, app_links delivers the URI, and the router maps its path to a screen. Keep one list of supported paths in the router and send anything unknown to the home screen, so an old link from last year's email never lands on an empty page. If a path needs a signed in user, store the target before redirecting to login and resume it once the session exists. That small detail is what makes deep linking in Flutter feel reliable to users, because a shared link always ends where the sender meant it to.
Deep linking in Flutter: the things that break silently
These are the failures we hit during testing. None of them produce a useful error message. We pulled together more of these hard won lessons in our writeup on mobile auth lessons from 10 apps, if you want the broader picture.
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 of deep linking in Flutter 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. If you are scoping a project like this and want a sense of the numbers involved, our guide to mobile app development cost breaks down what a build like this typically takes, and our Flutter app development company page shows how we run complete Flutter projects end to end.
How to test deep linking in Flutter before release
Every failure above is silent, so testing has to be deliberate. Clicking a link in your own inbox once is not a test. These are the checks we run on every project before a build goes to the stores.
Android
Fire the link directly from the command line instead of from an email. This shows whether the app opens the URL at all, without any email client in the way:
The second command lists the domains the system has verified for your package. If your domain shows anything other than verified, the problem is in assetlinks.json or the fingerprint inside it, not in your Dart code. Run it on a release build, because that is the signing key your users actually get.
iOS
Install a TestFlight build on a physical iPhone, paste the link into the Notes app and tap it. Links typed into the Safari address bar do not trigger Universal Links, so that test always looks like a failure even when the setup is correct.
If the app does not open, check what Apple actually fetched. Apple serves the association file through its own CDN, and you can request it at https://app-site-association.cdn-apple.com/a/v1/yourdomain.com. If that response is empty or out of date, the device never received your configuration, and reinstalling the app after the CDN updates is what picks up the change.
The full flow
Finally, send real confirmation emails to Gmail, Apple Mail and Outlook accounts and open each one on both platforms, once with the app installed and once without it. That matrix is where the redirect page earns its place, and it takes less than an hour on a project where deep linking in Flutter sits on the login path.
Deep linking in Flutter: FAQ
Does deep linking in Flutter work without extra setup?
No. Flutter handles the screens, but deep linking needs platform configuration on Android and iOS, verification files on your domain and Dart code that reads the incoming URL. Without all of them, links open the browser instead of the app.
What is the difference between URI schemes and Universal Links?
A URI scheme such as myapp:// works between apps on one device, but email clients do not trust it. Universal Links on iOS and App Links on Android are normal https links on your own domain, which the system opens in the app when it is installed.
Which package should I use for deep linking in Flutter?
We use app_links. From version 6 its uriLinkStream covers both cold start and warm start, so one listener is enough, as long as you subscribe before runApp().
How do I test deep linking in Flutter on a real device?
On Android, fire the link with adb shell am start and check domain verification with adb shell pm get-app-links on a release build. On iOS, install a TestFlight build on a physical iPhone and tap the link from the Notes app, because the Simulator and the Safari address bar do not trigger Universal Links.
Why do deep links work in debug but not in production?
On Android the most common cause is a debug keystore fingerprint in assetlinks.json. Release builds are signed with a different key, so add the release SHA-256 fingerprint as well. On iOS, Universal Links never work in the Simulator, so test on a real device.


