The Flutter Kit logoThe Flutter Kit
Tutorial

Flutter Deep Linking 2026: Universal Links and App Links End-to-End

The practical 2026 Flutter deep linking guide. Universal Links, App Links, app_links 5.x plus go_router 14.x, deferred deep links, paywall entry points, and failure-mode debugging.

Ahmed GaganAhmed Gagan
16 min read

Deep linking is the feature that makes marketing, referrals, paywalls, and user onboarding actually work in a mobile app. And it is the single most-painful Flutter setup in 2026. The legacy guides are out of date (iOS 14 Associated Domains, old uni_links package, pre-go_router routing) and the new 2026 stack is not well documented. This post walks through the entire setup end to end using app_links 5.x plus go_router 14.x, including the domain-verification JSON files Apple and Google require on your website.

Short version: you need two files served from your website, one entitlement on iOS, one intent filter on Android, and one package in your Flutter app that merges cold-start and warm-start deep link events into a single stream. Wired correctly, tapping a link liketheflutterk.it.com/r/abc123 opens the app on the referral screen with the code populated, whether the user tapped the link in Safari, Messages, Gmail, Instagram, or Twitter.

Custom schemes vs Universal Links vs App Links

Three mechanisms exist and 2026 best practice picks the right one for the right job.

MechanismURL shapeWhere it works2026 verdict
Custom schememyapp://offer/123Inside the app only (not shareable in browsers/email)Use for internal-only deep links (ActivityPub, background callbacks)
Universal Links (iOS)https://theflutterk.it.com/offer/123Safari, Messages, Gmail, Slack, Twitter, every iOS appRequired for any iOS marketing link
App Links (Android)https://theflutterk.it.com/offer/123Chrome, Messages, Gmail, every verified Android appRequired for any Android marketing link

Use HTTPS links (Universal Links plus App Links) for everything user-facing. Fall back to custom scheme only for in-app flows where HTTPS does not apply.

iOS setup: the apple-app-site-association file

Apple's Universal Links require a JSON file athttps://your-domain.com/.well-known/apple-app-site-association. The file is served as plain JSON (not gzipped, not a redirect, not behind auth). Apple fetches it during install and verifies the handshake.

The minimum content:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.yourcompany.yourapp"],
        "components": [
          { "/": "/offer/*", "comment": "Marketing offer pages" },
          { "/": "/r/*", "comment": "Referral links" },
          { "/": "/onboarding", "comment": "Onboarding flow entry" }
        ]
      }
    ]
  }
}

Three gotchas that will burn you:

  • Serve with Content-Type: application/json. If your CDN servesapplication/pkcs7-mime for this path, Apple silently ignores the file. Vercel, Netlify, and Cloudflare all default to this correctly if you put the file atpublic/.well-known/apple-app-site-association.
  • No file extension. The file name is literally apple-app-site-association with no.json suffix.
  • Apple caches the file aggressively. Users need to reinstall or wait up to 24 hours after a change. During development use the Associated Domains entitlement withapplinks: prefix plus mode=developer for faster testing.

iOS app entitlement

In Xcode, select your target, Signing & Capabilities, click + Capability, add Associated Domains. Add entries of the form applinks:theflutterk.it.com. Ship both the bare domain and any subdomain your marketing uses.

For TestFlight and local development you can append ?mode=developer which bypasses Apple's caching:

applinks:theflutterk.it.com?mode=developer

Never ship the developer-mode entry in production. Strip it in your Fastlane or CI build phase.

Android setup: the assetlinks.json file

Android's App Links verification requires a JSON file athttps://your-domain.com/.well-known/assetlinks.json. The content includes your package name and the SHA-256 fingerprints of your signing keys.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourcompany.yourapp",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:...your_debug_key_fingerprint...",
        "DD:EE:FF:...your_release_key_fingerprint...",
        "11:22:33:...your_play_app_signing_fingerprint..."
      ]
    }
  }
]

Include three fingerprints: your local debug key, your upload key, and the Play App Signing key. Android verifies against all three. Get the Play App Signing fingerprint from Play Console under Release, Setup, App Signing.

Android manifest configuration

In android/app/src/main/AndroidManifest.xml, add an intent filter to your main activity:

<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="theflutterk.it.com" />
</intent-filter>

android:autoVerify="true" triggers Android's background verification against assetlinks.json on first install. If the verification fails, your app no longer opens the link automatically (the user sees a chooser). Monitor verification status:

adb shell pm get-app-links com.yourcompany.yourapp

Wiring app_links plus go_router in Flutter

The app_links package (version 5.x in 2026) is the de facto Flutter SDK for handling both iOS Universal Links and Android App Links. It exposes two streams: an initial link from cold start, and a stream of subsequent links while the app is running.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  app_links: ^5.0.0
  go_router: ^14.0.0

In main.dart, wire the stream into go_router:

import 'package:app_links/app_links.dart';
import 'package:go_router/go_router.dart';

final _appLinks = AppLinks();

final _router = GoRouter(
  initialLocation: '/',
  routes: [ /* your routes */ ],
);

Future<void> initDeepLinks() async {
  // Cold start
  final initial = await _appLinks.getInitialLink();
  if (initial != null) _router.go(initial.path + (initial.query.isEmpty ? '' : '?' + initial.query));

  // Warm start
  _appLinks.uriLinkStream.listen((uri) {
    _router.go(uri.path + (uri.query.isEmpty ? '' : '?' + uri.query));
  });
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initDeepLinks();
  runApp(MyApp());
}

Two tricks that matter:

  • Parse the URI yourself before handing to go_router. Stripping the host means go_router only sees the path plus query, which matches your route definitions cleanly.
  • Hold the initial link in state until the user is authenticated. Do not navigate to/offer/123 if the user is on the login screen; queue the link and consume it after login.

Handling deferred deep links

A deferred deep link is one where the user taps a link but does not have the app installed. They install, launch, and expect to land on the linked content. Apple and Google do not support this natively. Third-party services solve it.

ServiceFree tierFlutter SDK quality (2026)Best for
Firebase Dynamic LinksSunset August 2025 (do not use)N/ALegacy apps only
BranchGenerous free tierMature, stable Flutter SDKMarketing-heavy apps, referrals
AdjustPaid onlyGood but enterprise-focusedApps with real ad spend
AppsFlyerPaid onlyGood, heavier SDKAttribution-first apps
Roll your own (clipboard + API)FreeDIYSimple referral-only flows

My default recommendation in 2026: Branch if you need deferred deep links, roll-your-own if you only need referrals. Firebase Dynamic Links was the go-to but Google sunset it August 2025; migrate if you have not already.

Testing deep links on simulator and device

Three testing patterns:

  • iOS Simulator: xcrun simctl openurl booted https://theflutterk.it.com/offer/123
  • Android Emulator: adb shell am start -a android.intent.action.VIEW -d "https://theflutterk.it.com/offer/123"
  • Real device: send yourself the link via Messages or Mail, then tap. Do NOT paste into Safari address bar on iOS: Apple explicitly blocks Universal Links opened that way.

Paywall and onboarding entry points

The two deep-link patterns that actually move revenue:

  • Paywall entry. A push notification or ad creative links tohttps://theflutterk.it.com/paywall?source=push&offer=lifetime. Your app reads the query params, logs the source in analytics, and shows the right paywall variant. Use this for retention campaigns.
  • Invite / referral entry. A shared link likehttps://theflutterk.it.com/r/alice123 drops the user into signup with the referral code pre-filled. After signup, attribute the referral back to Alice and credit both users.

A sample go_router route for the paywall pattern:

GoRoute(
  path: '/paywall',
  builder: (context, state) => PaywallScreen(
    source: state.uri.queryParameters['source'] ?? 'organic',
    offerId: state.uri.queryParameters['offer'] ?? 'monthly',
  ),
),

Common failure modes and how to debug

  • Tapping the link opens Safari instead of the app (iOS). Usually a badapple-app-site-association. Check withhttps://app-site-association.cdn-apple.com/a/v1/your-domain.com which Apple serves publicly once verified.
  • Tapping opens a chooser dialog (Android). assetlinks.jsonfingerprint mismatch. Re-runadb shell pm get-app-links <package> and check verification status. Ifverified is false, the fingerprints do not match.
  • Cold-start link is null. You called getInitialLink too early. Ensure WidgetsFlutterBinding.ensureInitialized() ran first.
  • Warm-start link fires twice. You have two subscribers touriLinkStream. Subscribe exactly once, at app level, and distribute via a BLoC or Riverpod provider.
  • Link opens on web instead of app on Instagram. Instagram in-app browser sometimes strips the scheme. No fix; educate users to tap the three dots and "Open in Browser."

A minimum-viable deep-link architecture

Put every deep-link handler behind a single service. Your feature code should not directly subscribe to uriLinkStream.

abstract class DeepLinkService {
  Stream<Uri> get incomingLinks;
  Future<Uri?> getInitialLink();
}

class AppLinksDeepLinkService implements DeepLinkService {
  final _inner = AppLinks();
  @override
  Stream<Uri> get incomingLinks => _inner.uriLinkStream;
  @override
  Future<Uri?> getInitialLink() => _inner.getInitialLink();
}

The service gets injected into a DeepLinkBloc that decides what to do with each URI. If the user is unauthenticated, queue the link. If it is a known route, navigate. If it is unknown, log and ignore. This abstraction pays for itself the first time you switch from rawapp_links to Branch.

What The Flutter Kit ships

The Flutter Kit ships the full deep-linking stack pre-wired:app_links plus go_router plus a DeepLinkBloc that queues links during auth flows, a sample apple-app-site-association template atpublic/.well-known/, an assetlinks.json template, Fastlane phases that strip the ?mode=developer entitlement on release builds, and three example deep routes (paywall, referral, onboarding) that demonstrate query-param handling.

$69 one-time, unlimited commercial projects. See every integration on the features page or jump to checkout.

Final checklist

  • apple-app-site-association at /.well-known/, no extension, JSON content-type
  • Associated Domains entitlement in Xcode with applinks: prefix
  • Release build strips ?mode=developer entitlement
  • assetlinks.json at /.well-known/ with three SHA-256 fingerprints (debug, upload, Play App Signing)
  • Android manifest intent filter with android:autoVerify="true"
  • app_links 5.x + go_router 14.x wired in main.dart
  • Initial link handler runs after WidgetsFlutterBinding.ensureInitialized()
  • Deep-link queueing during auth flows
  • Branch or similar service integrated if you need deferred deep links
  • Tested on simulator, emulator, and two real devices across Messages, Gmail, Safari, and Chrome
Share this article

Ready to ship your Flutter app faster?

The Flutter Kit gives you a production-ready Flutter codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Flutter Kit