Implementing Universal and App Links for Your App
A step-by-step guide to seamless deep linking on iOS and Android
Introduction
Have you ever clicked a link in an email, ad, or WhatsApp message that opened an app directly on a specific screen — like a post, quiz, or checkout page? That seamless experience is powered by deep linking.
Universal Links (iOS) and App Links (Android) are the modern standard for deep linking in mobile apps. They allow your users to open precise in-app destinations straight from the web — without detours, prompts, or friction. Whether it’s onboarding from a referral campaign or re-engaging an existing user from an email, deep links bridge the gap between your app and the rest of the digital world.
In this article, we’ll explore how Universal and App Links work, why they’re essential, and how to implement them step-by-step using a shared domain like app.example.com for both iOS and Android.
1. What Are Deep Links?
A deep link is any URL that opens your app to a specific screen or context, rather than just the home page.
For example: https://app.example.com/post/{post_id}
might open directly to the Post Detail screen in your app.
Deep Links vs Normal Links
| Type | Opens | Behavior |
|---|---|---|
| Normal Link | Web browser | Always opens a webpage |
| Deep Link | Mobile app (if installed) | Opens a specific screen; falls back to web if not installed |
The Different Types of Deep Links
1. Custom Scheme Links
Example: com.example.<package_name>://post/{post_id} These use your own URI scheme instead of https://.
The OS checks whether an app has registered the com.example.<package_name>:// scheme, and if so, it routes the link to that app.
Pros
- Simple to set up.
- Works offline or from app-to-app communication.
Cons
- Not shareable via browsers or social apps.
- No fallback to the web.
- Can trigger “Open with” dialogs.
Use case: Internal deep navigation within your ecosystem or integrations between apps you control.
2. Universal Links (iOS)
Introduced by Apple in iOS 9, these are HTTPS URLs associated with your app through a verified domain.
Example:
https://app.example.com/post/{post_id}
When a user taps this link:
- iOS checks if any installed app claims the domain.
- If verified, your app opens directly on that route.
- If not installed, Safari opens the same link as a webpage.
Why it’s better: No prompt, secure, shareable, and SEO-friendly.
3. App Links (Android)
Android’s equivalent of Universal Links. They’re also HTTPS URLs that verify ownership via a JSON file (assetlinks.json).
Benefits:
- No user prompt — opens the app directly.
- The same domain works across both app and web.
- Integrates smoothly with marketing campaigns and Play Store installs.
4. Deferred Deep Links
These preserve context even if the app isn’t installed yet.
Example flow:
- User clicks a referral link → no app → redirected to store.
- Installs the app → first launch opens the target screen (like
/post/).
Usually implemented via Firebase Dynamic Links, Branch, or AppsFlyer.
3. Why Universal and App Links Matter
1. Frictionless User Experience: Users land exactly where they expect.
2. Cross-Platform Consistency: One URL for both web and app.
3. Better Attribution: Add campaign or referral parameters like ?utm_source=instagram.
4. Improved Retention: Re-engage dormant users directly into meaningful content.
5. SEO & Trust: HTTPS links maintain ranking and credibility.
4. Architecture Overview
A unified deep link setup involves three layers:
1. OS Level:
- iOS verifies
apple-app-site-association. - Android verifies
assetlinks.json.
2. App Level:
- Flutter / native code listens for URIs and routes accordingly.
3. Server Level (optional):
- Redirect uninstalled users to App Store / Play Store.
- Track analytics and campaign metadata.
user clicks link → OS verification → app opens → screen displayed.
5. Implementation Step-by-Step
Let’s implement deep links for app.example.com.
Step 1. Configure Android (App Links)
In AndroidManifest.xml:
<activity>
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true" <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="app.example.com" </intent-filter> <!-- Optional custom scheme -->>
<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="{package_name}" /> </intent-filter></activity>
Host this file:https://app.example.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "{package_name}",
"sha256_cert_fingerprints": [
"12:34:56:78:90:AB:CD:EF:..."
]
}
}]
Tip: Use your release keystore SHA-256 fingerprint.
Validate setup at Digital Asset Links Tool.
Step 2. Configure iOS (Universal Links)
- Enable Associated Domains in Xcode → Signing & Capabilities.
Add:applinks:app.example.com - Host this file at
https://app.example.com/apple-app-site-association
(no.jsonextension){
"applinks": {
"apps": [],
"details": [{
"appID": "ABCDE12345.{package_name}",
"paths": ["/post/*", "/quiz/*", "/product/*"]
}]
}
} - Serve headers:
Content-Type: application/json
No redirects are allowed — the contentmust be served via HTTPS directly.
Step 3. Handle Incoming Links in Your App (Example: Flutter)
Install: flutter pub add uni_links
In your main widget:
import 'dart: async';
import 'package: flutter/material.dart';
import 'package:uni_links/uni_links.dart';
StreamSubscription? _sub;
void initDeepLinks(BuildContext context) async {
// Cold start
final initialUri = await getInitialUri();
if (initialUri != null) handleUri(initialUri, context);
// Listen for warm links
_sub = uriLinkStream.listen((uri) {
if (uri != null) handleUri(uri, context);
}, onError: (err) {
debugPrint('Deep link error: $err');
});
}
void handleUri(Uri uri, BuildContext context) {
if (uri.pathSegments.isEmpty) return;
switch (uri.pathSegments.first) {
case 'post':
final postId = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null;
if (postId != null) Navigator.pushNamed(context, '/post', arguments: postId);
break;
case 'quiz':
Navigator.pushNamed(context, '/quiz');
break;
default:
Navigator.pushNamed(context, '/home');
}
}
Testing cold start:
Launch the app via:
adb shell am start -W \
-a android.intent.action.VIEW \\
-d "https://app.example.com/post/{post_id}"
{package_name}
Step 4. Optional Redirect Service (Fallback)
For uninstalled users, create a lightweight backend redirect:
@app.route("/open")def open_link(): post_id = request.args.get("post_id") ua = request.headers.get("User-Agent") if "Android" in ua or "iPhone" in ua: return redirect(f"https://app.example.com/post/{post_id}") else: return redirect("https://play.google.com/store/apps/details?id=com.{package_name}")
This also enables campaign tracking and analytics.
📈 Visual 2 (Recommended):
A flowchart showing: user click → OS verification → app open OR store fallback.
6. Common Issues & Fixes
| Problem | Likely Cause | Solution |
|---|---|---|
| iOS opens Safari | AASA not verified or wrong MIME | Serve JSON with Content-Type: application/json |
| Android opens browser | Missing or invalid assetlinks.json | Host file under /.well-known/ with correct SHA-256 |
| Link ignored on cold start | App not listening early enough | Handle initial URI in initState() |
| Works only in debug | Wrong keystore fingerprint | Use release certificate fingerprint |
7. Testing Your Setup
iOS
- Open Safari → type your link → app should open.
- Check Console logs (filter
swcdfor Universal Link activity).
Android
- Test via
adb(command above). - Check Settings → Apps → “Open by Default” → ensure domain verified.
8. Advanced: Deferred Deep Linking
To maintain context after installation:
- Use Firebase Dynamic Links or AppsFlyer SDK.
- The link includes metadata (like
postId) that’s restored on first launch. - Flutter snippet (Firebase):final data = await FirebaseDynamicLinks.instance.getInitialLink();
final deepLink = data?.link;
if (deepLink != null) handleUri(deepLink, context);
9. Key Takeaways
-> Universal and App Links provide a seamless bridge between web and app.
-> Use a single verified domain — app.example.com — for consistency.
-> Always test both warm and cold launches.
-> Add a redirect layer for analytics and store fallbacks.
-> Combine with deferred deep links for campaign continuity.
10. Author’s Note
Implementing deep links transformed how our users reached specific content inside the EverWiz app. Using app.example.com as a unified domain allowed us to manage both iOS Universal Links and Android App Links effortlessly — improving user re-engagement.