Marketing sent the email. The link opened the app store instead of the promo screen. Support tickets mentioned "the link worked on Android but not iPhone." QA could reproduce it only on a physical device, never in the simulator.
If you are new to Flutter deep linking, that story probably sounds familiar.
What is a deep link? A URL that opens your app and navigates to a specific screen, not just the home screen. Example: https://example.com/promo/123 should open your app directly on the promo details page.
Why beginners struggle: three separate systems must agree before that works:
- Your website (verification files that prove you own the domain)
iOS or Android native config (entitlements, manifest entries)- Your Flutter router (Dart code that maps the URL path to a screen)
Deep links look simple in a demo. In production, one misconfigured file fails silently.
We ship overview.
How a deep link works (mental model)
When someone taps a link, this is the flow:
User tapshttps://example.com/promo/123in email, SMS, or a browser.
Phone checks whether your app is allowed to handle that domain (verification files on your server + native app config).
OS opens the app (if installed and verified) and passes the full URL to Flutter.
Your router reads the path (/promo/123) and shows the matching screen.
Two terms you will see everywhere:
Cold start: the app was fully closed. The OS launches it and delivers the link shortly after.
Warm start: the app was in the background. The OS wakes it and passes a new link while it is already running.
Cold start is where most beginner bugs hide (auth not ready, router not listening yet).
Terms you'll see in every tutorial
| Term | Plain meaning |
|---|---|
| Deep link | URL that opens a specific in-app screen |
| Universal Link (iOS) | HTTPS deep link verified by Apple |
| App Link (Android) | Same idea on Android |
| AASA | Apple's domain verification file (apple-app-site-association) on your website |
| assetlinks.json | Android's domain verification file on your website |
| Custom URL scheme | myapp:// shortcut; easy to demo locally, weak for email campaigns |
| Intent filter | Android manifest entry that says "this app handles these URLs" |
Official setup guides for each platform are in References at the end.
When Deep Links Matter (And When They Do Not)
You do not need deep links on day one of every app. Add them when a URL in email, SMS, or ads should land on a specific in-app screen.
Worth the setup cost:
- Passwordless login or magic links (login email opens the app to the confirm screen)
- Referral and invite flows (friend's invite link opens signup with code pre-filled)
- Push notification taps to specific content (notification opens the order detail screen)
- Email campaigns to product screens (newsletter link opens the sale page in-app)
- Web-to-app handoff from marketing sites (landing page link opens the app if installed)
Often overbuilt early:
- Every blog URL mapping to an in-app WebView
- Complex deferred deep linking before v1 has retention data
- Custom URL schemes as the only path (see below)
Scope the first release. Ship Universal Links / App Links for two or three high-value paths, then expand.
Custom Schemes vs Universal / App Links
If you are new to Flutter deep linking, start with verified HTTPS links on a domain you own.
| Mechanism | Example | Pros | Cons |
|---|---|---|---|
| Custom URL scheme | myapp://promo/123 | Fast to prototype | Not verified; conflicts with other apps; email clients may block |
| iOS Universal Links | https://example.com/promo/123 | Verified domain; opens app if installed | Requires apple-app-site-association (AASA) file |
| Android App Links | https://example.com/promo/123 | Verified domain; avoids disambiguation dialog | Requires assetlinks.json |
Production default: HTTPS links on a domain you control, with platform verification files served correctly. Keep a custom scheme only as fallback for legacy campaigns if needed.
Beginner recommendation: prototype with a custom scheme locally if that helps you learn routing. Ship real campaigns with Universal Links / App Links.
Platform setup walkthroughs: .
iOS: Universal Links Checklist
Associated Domains entitlement
In plain terms: tells iOS "this app is allowed to open links from example.com."
Where to click in Xcode: Runner target → Signing & Capabilities → Associated Domains → add entries like (see Apple's .
Flutter handling
In plain terms: Dart code that receives the URL from iOS and calls your router to show the right screen.
Packages commonly used: for default handler behavior.
If you use a third-party link plugin, opt out of Flutter's built-in handler: set FlutterDeepLinkingEnabled to NO in Info.plist. Otherwise both handlers may fire and cause double navigation.
Pattern:
- Listen for initial link on cold start (
getInitialLink) - Subscribe to stream for warm start (
uriLinkStream) - Parse path and query params
- Navigate via your router (
go_router,auto_route, etc.)
Cold start race: routing may run before auth state loads. Queue the pending deep link until session is ready, then navigate once.
Android: App Links Checklist
Intent filters in AndroidManifest.xml
In plain terms: tell Android which URLs should open your app instead of the browser.
Where to edit: android/app/src/main/AndroidManifest.xml, inside the <activity> tag for .MainActivity.
For verified App Links, use android:autoVerify="true" on intent filters with https scheme, host, and pathPrefix or pathPattern (see .
Run (see .
Normalize paths: marketing may send trailing slashes, UTM query strings, or mixed casing. Strip tracking params before route matching.
Guard routes: if /account/orders/:id requires login, redirect to sign-in with return path stored, then resume after auth.
Backend and Web Fallback URLs
Deep links often start on the web. Plan three outcomes:
App installed: open app to target screen (Universal / App Link)
App not installed: show mobile web equivalent or store landing page
Desktop click: responsive web page, not a broken custom scheme
Your web server should serve:
- AASA and assetlinks files
- Smart app banner meta tags optional on iOS Safari
- Consistent URL structure between web routes and in-app routes
Backend teams own path conventions. If web uses /promo/123 but Flutter expects /promotions?id=123, campaigns will fail despite valid platform files. Align to catch manifest, AASA, and assetlinks misconfigurations early.
When it breaks (and what you'll see)
These bugs often look like Flutter routing problems but are usually config issues on the website or native side.
AASA served with redirect
Symptom: link always opens Safari instead of your app.
CDN or www redirect breaks iOS verification. AASA URL must return 200 directly. Apple caches AASA via its CDN ( for uppercase SHA-256, redirect, and Play App Signing mismatches.
Query param stripping by email ESP
Symptom: link opens the app but lands on the wrong screen or home screen.
Use path params for critical IDs when possible; treat query as optional metadata.
Router navigates before auth
Symptom: user lands on login screen; promo or order screen never appears.
User lands on protected screen, bounced to login, loses deep link context.
Multiple listeners
Symptom: screen flashes or navigates twice.
Two plugins subscribing cause double navigation or race crashes. One ownership layer for incoming links.
Pre-Release Checklist
Work top to bottom: website files → native config → Flutter router → device tests.
- [ ] Domain hosts valid AASA and assetlinks.json without redirects
- [ ] All marketing hosts (
www, apex) covered in entitlements and intent filters - [ ] Release signing fingerprints in assetlinks.json (Play App Signing aware)
- [ ] Flutter router maps every campaign path to a screen
- [ ] Auth-gated routes queue and resume deep links
- [ ] Web fallback pages exist for app-not-installed case
- [ ] Tested cold/warm start on physical iOS and Android
- [ ] Tested from email and SMS, not only pasted URLs
- [ ] Analytics events on deep link open and conversion funnel
- [ ] Runbook for marketing: approved URL format doc shared with growth team
Key Takeaways
- A deep link is a URL that opens a specific screen, not just your app icon.
- Use verified HTTPS links (Universal Links / App Links) for real campaigns, not custom schemes alone.
- Your website, native config, and Flutter router must all agree on the same paths.
AASA (iOS) and assetlinks.json (Android) must be served correctly on every campaign host, with no redirects.- Test on real devices from email and SMS, not only simulators; cold start is where most first-time bugs show up.
Which broke last on your project: iOS verification, Android autoVerify, or Flutter routing after cold start?
If deep links are in scope for your next release, scoping a mobile product with deep link requirements early keeps platform files, router design, and campaign URLs on the same checklist before store submission.
SOCIAL SHARE CARD GENERATOR