Flutter App Links & Universal Links: Automated Domain Verification and CI Deep-Link Testing

Deep linking is critical for user onboarding, marketing attribution, push notification navigation, and payment redirects in mobile apps. Legacy custom URL schemes (myapp://) carry significant hijacking vulnerabilities where malicious third-party apps register identical schemes to intercept sensitive payloads.
To counter this threat, Google and Apple enforce domain-bound verification standards: Android App Links and iOS Universal Links.
However, minor hosting mistakes on .well-known endpoints—such as incorrect HTTP Content-Type, SSL validation errors, or mismatched SHA-256 fingerprints—frequently cause deep links to fail silently in production, defaulting to mobile web browsers.
This guide provides an architectural breakdown of App Links and Universal Links, Flutter client handling with GoRouter and app_links, and an automated GitHub Actions CI pipeline to verify domain association before deployment.
Key Takeaways
- Android App Links Requirements: The file
https://<domain>/.well-known/assetlinks.jsonmust serveContent-Type: application/jsonand include both local keystore and Google Play App Signing SHA-256 fingerprints.- iOS Universal Links Requirements: The file
https://<domain>/.well-known/apple-app-site-association(AASA) must list validappIDvalues formatted as<TEAM_ID>.<BUNDLE_ID>.- Flutter Router Integration: Combining
app_linkswithGoRouterensures parameter retention across cold launches and background app states.- CI Test Automation: Utilizing GitHub Actions with Google Statement List APIs and CLI triggers (
adb/xcrun simctl) catches broken links before reaching production.
1. Domain Association File Configurations
1) Android assetlinks.json (/.well-known/assetlinks.json)
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "dev.effidev.flutterapp",
"sha256_cert_fingerprints": [
"14:6D:E9:7D:59:06:50:57:3E:99:A5:1D:64:1D:64:D4:6C:C9:86:EC:65:4E:91:D9:D2:ED:EB:98:C4:4B:9B:C6"
]
}
}
]
2) iOS apple-app-site-association (/.well-known/apple-app-site-association)
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.dev.effidev.flutterapp",
"paths": [ "NOT /api/*", "/product/*", "/share/*" ]
}
]
}
}
2. Flutter Client Handling (GoRouter + app_links)
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:app_links/app_links.dart';
final _appLinks = AppLinks();
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id'] ?? '';
return ProductDetailScreen(productId: id);
},
),
],
);
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_initDeepLinks();
}
Future<void> _initDeepLinks() async {
// 1. Cold Start Initial Link
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
router.go(initialUri.path);
}
// 2. Background State Stream
_appLinks.uriLinkStream.listen((uri) {
router.go(uri.path);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
3. GitHub Actions CI Automated Verification
name: Verify Deep Links Domain Association
on:
push:
branches: [ main ]
jobs:
verify-domain:
runs-on: ubuntu-latest
steps:
- name: Check Android Assetlinks.json HTTP & SSL
run: |
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://effidev.dev/.well-known/assetlinks.json)
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "Error: assetlinks.json HTTP status is $HTTP_STATUS"
exit 1
fi
- name: Check iOS Apple App Site Association (AASA)
run: |
AASA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://effidev.dev/.well-known/apple-app-site-association)
if [ "$AASA_STATUS" -ne 200 ]; then
echo "Error: AASA file HTTP status is $AASA_STATUS"
exit 1
fi
Testing Links via CLI
-
Android CLI:
adb shell am start -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "https://effidev.dev/product/456" -
iOS Simulator CLI:
xcrun simctl openurl booted "https://effidev.dev/product/456"
Conclusion
Automating domain verification in CI pipelines eliminates production failures for App Links and Universal Links, providing smooth, reliable user navigation.