flutter-app-links-deep-link-engineering
Skill8 starsChanged 48 days agoMITView on GitHub
Design, implement, review, secure, debug, migrate, and productionize Flutter deep-link infrastructure using app_links across Android, iOS, macOS, Windows, Linux, and Web. Use this skill whenever work involves app_links, Android App Links, iOS Universal Links, custom URL schemes, web-to-app activation, cold-start/runtime link delivery, go_router or Navigator deep-link integration, auth-gated links, pending destinations, duplicate navigation, URI validation, assetlinks.json, AASA, release-signing association, or deep-link security—even when the user only reports that a link opens the browser, opens twice, is lost after login, or fails only in release.
In this repository70 other files it gives its agents.
- camerawesome-flutterSkill
- dart-args-production-cliSkill
- dart-flutter-equatable-engineeringSkill
- dart-flutter-ffi-systems-engineeringSkill
- dart-metaSkill
- document-camera-frameSkill
- document-scan-flutterSkill
- figma-to-flutter-expertSkill
- flutter-animations-production-engineeringSkill
- flutter-app-hardening-reverse-engineeringSkill
- flutter-app-icon-engineeringSkill
- flutter-application-security-engineeringSkill
- flutter-cachingSkill
- flutter-concurrency-memory-performanceSkill
- flutter-confetti-engineeringSkill
- flutter-dart-package-engineeringSkill
- flutter-design-system-from-design-mdSkill
- flutter-dio-cache-interceptor-engineeringSkill
- flutter-dio-engineeringSkill
- flutter-doc-scannerSkill
- flutter-drift-persistence-engineeringSkill
- flutter-expert-tipsSkill
- flutter-firebase-analyticsSkill
- flutter-firebase-core-engineeringSkill
- flutter-firebase-messagingSkill
- flutter-get-it-injectable-dependency-injectionSkill
- flutter-go-router-engineeringSkill
- flutter-hive-persistence-engineeringSkill
- flutter-image-performanceSkill
- flutter-image-picker-production-engineeringSkill
- flutter-lints-static-analysis-engineeringSkill
- flutter-local-notifications-production-engineeringSkill
- flutter-memory-leak-engineeringSkill
- flutter-mobile-developmentSkill
- flutter-motion-engineeringSkill
- flutter-multi-environmentSkill
- flutter-native-splash-engineeringSkill
- flutter-package-plugin-selectionSkill
- flutter-path-provider-production-storageSkill
- flutter-performance-jank-optimizationSkill
- flutter-permission-handler-engineeringSkill
- flutter-pinputSkill
- flutter-production-build-releaseSkill
- flutter-production-engineeringSkill
- flutter-production-loggingSkill
- flutter-pro-image-editor-engineeringSkill
- flutter-secure-storage-security-engineeringSkill
- flutter-sentry-engineeringSkill
- flutter-share-plus-engineeringSkill
- flutter-skeletonizer-engineeringSkill
- flutter-squadron-concurrency-engineeringSkill
- flutter-supabase-engineeringSkill
- flutter-svg-engineeringSkill
- flutter-ui-performance-engineeringSkill
- flutter-url-launcher-engineeringSkill
- flutter-video-player-production-engineeringSkill
- flutter-webview-production-engineeringSkill
- freezed-production-engineeringSkill
- html-to-flutterSkill
- hydrated-blocSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
55 rules. About 0 are common, 35 are distinctive, and 20 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈35
Distinctive · estimate
This author's own choices. Worth a look.
20
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 9
Use this governing model:
- Distinctive: line 23
Never collapse these layers.
- Distinctive: line 28
Treat every incoming URI as untrusted external input.
- Distinctive: line 32
Do not pass arbitrary raw external URLs deep into business logic.
- Distinctive: line 33
Do not navigate until application readiness is resolved.
- Distinctive: line 39
Prefer verified HTTPS App/Universal Links for sensitive public flows when practical.
- Distinctive: line 40
Never execute destructive side effects directly from the link callback.
- Distinctive: line 41
Never log reset tokens, magic codes, invitation secrets, auth codes, or complete sensitive URLs.
- Distinctive: line 42
Test cold start, runtime delivery, auth deferral, and production association.
- Distinctive: line 43
Verify release signing/domain association rather than assuming debug success proves production readiness.
- Distinctive: line 44
Treat published URL formats as compatibility contracts.
- Very specific: line 45
Keep app_links vendor/package APIs at the infrastructure boundary.
- Distinctive: line 55
Read the smallest relevant reference set.
- Distinctive: line 95
Do not add another listener until ownership is understood.
- Very specific: line 107
Do not add app_links for:
- Very specific: line 112
If app_links owns platform delivery, disable competing Flutter default deep-link handling using the configuration required by the pinned …
- Distinctive: line 134
Do not refactor public URLs like internal class names.
- Distinctive: line 137
Keep association failures separate from Dart routing failures.
- Very specific: line 148
If Play App Signing is used, verify the App Signing certificate used on installed production builds rather than blindly using a local …
- Very specific: line 149
Use device verification tooling such as pm get-app-links when diagnosing association.
- Very specific: line 160
Do not promise that a matching Universal Link always opens the app.
- Distinctive: line 163
Read the platform reference before implementation.
- Distinctive: line 170
Prefer one app-scoped source/coordinator:
- Very specific: line 179
Do not subscribe in build().
- Very specific: line 193
If using both, route both through the same handler and deduplicate.
- Very specific: line 194
Use getLatestLink() only for an explicit latest-link/diagnostic requirement.
- Very specific: line 197
Use Uri or Uri.tryParse. Reject before route resolution if any required property fails:
- Distinctive: line 207
Never validate domains with:
- Very specific: line 229
Keep raw protocol details out of repositories/widgets/business services after this point unless the protocol specifically requires the …
- Distinctive: line 230
Use explicit stable intent categories instead of substring route chains.
- Distinctive: line 239
Use a bounded pending-intent model:
- Very specific: line 259
Never let ?next= become an unrestricted route or external redirect.
- Distinctive: line 270
Use a short-window canonical-URI deduplicator for navigation duplication when useful.
- Very specific: line 284
If using go_router deliberately after app_links intake:
- Very specific: line 290
Do not call router.go(uri.toString()) on arbitrary external input.
- Very specific: line 291
If Router/go_router already owns platform deep links, avoid adding the second app_links path.
- Distinctive: line 295
Treat reset, magic-login, invitation, email-verification, OAuth, and payment-related URLs as credentials or security-sensitive protocol …
- Distinctive: line 296
Do not log complete URLs containing secrets.
- Distinctive: line 304
Prefer verified HTTPS flows for sensitive public links when practical.
- Distinctive: line 305
Do not execute destructive actions solely because the OS delivered a link.
- Distinctive: line 324
Keep platform delivery failure distinct from application route failure.
- Very specific: line 342
Use a fake DeepLinkSource rather than platform channels.
- Distinctive: line 353
Test actual terminated and running application flows.
- Distinctive: line 368
When a link does not work, locate the failing layer:
- Very specific: line 379
If an Android HTTPS link opens the browser, inspect App Link verification before changing Dart routes.
- Very specific: line 380
If an iOS Universal Link stays in Safari, inspect Associated Domains/AASA and browser/user context before blaming app_links.
- Distinctive: line 381
If a link is received twice, solve ownership first, deduplication second.
- Distinctive: line 384
When changing an existing deep-link system:
- Distinctive: line 392
update association files and web fallbacks
- Very specific: line 395
Do not regenerate native platform projects or replace the router merely to adopt app_links.
- Distinctive: line 398
When reviewing a repository, report:
- Distinctive: line 428
Do not call the implementation production-ready until applicable conditions are evidenced:
- Very specific: line 433
all external URIs cross scheme/host/path/parameter validation
- Distinctive: line 449
If an important production association, signing, authorization, or security condition is not verified, say so explicitly rather than …
- Distinctive: line 452
Start every deep-link task with:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Flutter app_links Deep-Link Engineering
Mission
Build one secure, testable deep-link pipeline from operating-system delivery to application navigation. Treat app_links as a platform link-delivery adapter only; keep routing, validation, authentication, authorization, deduplication, business idempotency, and user-facing semantics in explicit application layers.
Use this governing model:
external URI
→ OS association / registered scheme
→ app_links delivery
→ Uri parsing and origin validation
→ typed DeepLinkIntent
→ duplicate/idempotency gate
→ application readiness
→ authentication
→ authorization / backend validation
→ one router/navigation authority
→ destinationNever collapse these layers. A valid platform association does not make URI contents trusted, and a logged-in user is not automatically authorized for the linked resource.
Non-negotiable invariants
- Inspect the existing delivery path before adding
app_links. - Choose exactly one authoritative platform deep-link owner.
- Treat every incoming URI as untrusted external input.
- Parse with
Uri; do not split URLs manually. - Validate scheme, host, route shape, and parameters before routing.
- Convert accepted URIs to typed intents in non-trivial applications.
- Do not pass arbitrary raw external URLs deep into business logic.
- Do not navigate until application readiness is resolved.
- Revalidate pending intents after login/account changes.
- Authentication never replaces authorization.
- Possession of a link never proves resource permission.
- Make duplicate delivery harmless.
- Separate navigation deduplication from business-operation idempotency.
- Prefer verified HTTPS App/Universal Links for sensitive public flows when practical.
- Never execute destructive side effects directly from the link callback.
- Never log reset tokens, magic codes, invitation secrets, auth codes, or complete sensitive URLs.
- Test cold start, runtime delivery, auth deferral, and production association.
- Verify release signing/domain association rather than assuming debug success proves production readiness.
- Treat published URL formats as compatibility contracts.
- Keep
app_linksvendor/package APIs at the infrastructure boundary.
Load references only when needed
| Task | Read |
| Package role, lifecycle, initial/latest links, stream semantics, malformed URIs, parsing, dedup, typed intents | references/01-foundations-package-lifecycle.md |
| Android/iOS/macOS/Windows/Linux/Web registration, App Links, Universal Links, signing and association | references/02-platform-association.md |
go_router, Navigator, readiness, auth, authorization, pending intents, security, coordinator, idempotency, observability | references/03-routing-auth-security.md |
| Parser, coordinator, cold/warm/auth/duplicate/platform tests and debugging | references/04-testing-debugging.md |
| Anti-patterns, new implementation workflow, existing-code review, refactoring, production checklists | references/05-antipatterns-workflows-checklists.md |
| Read the smallest relevant reference set. For release-signing or association failures, read the platform reference before changing Dart routing code. |
Step 1 — Inspect the repository
Inspect before editing:
pubspec.yaml
pubspec.lock
lib/ routing/navigation/bootstrap code
every AppLinks() use
every uriLinkStream/stringLinkStream subscription
getInitialLink()/getLatestLink() usage
Flutter/go_router platform deep-link configuration
android/
AndroidManifest.xml
flavors/application IDs
signing configuration
ios/
Info.plist
entitlements
schemes/bundle IDs
macos/
windows/
linux/
web/
assetlinks.json deployment
apple-app-site-association deployment
tests
logging/analytics
notification/deferred-link integrationsRecord:
- Flutter and Dart versions
- locked
app_linksversion - router package/version
- target platforms
- public link domains and custom schemes
- production app/package/bundle identifiers
- production signing identities
- whether Flutter's built-in Router already owns platform links
- whether another plugin/native handler receives the same URLs Do not add another listener until ownership is understood.
Step 2 — Decide whether app_links should exist
Ask first:
Does Flutter Router/go_router already receive every required external URL?
├─ yes → keep the router-native path unless a concrete capability is missing
└─ no
└─ does app_links solve a real delivery gap?
├─ yes → use one app_links intake
└─ no → keep existing native/plugin architectureDo not add app_links for:
- internal
context.go()/ Navigator navigation - notification delivery
- deferred-install attribution
- a requirement already handled cleanly by Router/native code
If
app_linksowns platform delivery, disable competing Flutter default deep-link handling using the configuration required by the pinned Flutter/package version.
Step 3 — Define the public URI contract
Inventory the external formats the product promises to support, for example:
https://example.com/products/:id
https://example.com/invite/:token
https://example.com/reset-password/:token
myapp://open/account/billingFor each, define:
- scheme
- host
- path grammar
- parameter type/length
- repeated-query semantics
- fragment semantics if any
- authentication requirement
- authorization requirement
- token expiry/single-use behavior
- web fallback
- compatibility/versioning policy Do not refactor public URLs like internal class names. Existing emails, QR codes, partner links, indexed pages, and old app versions may depend on them.
Step 4 — Configure OS and hosting association
Keep association failures separate from Dart routing failures.
Android
For verified HTTPS links, verify:
manifest VIEW + DEFAULT + BROWSABLE filter
+ android:autoVerify=true
+ exact https host/path policy
+ /.well-known/assetlinks.json
+ exact application ID
+ exact installed production signing certificate fingerprintIf Play App Signing is used, verify the App Signing certificate used on installed production builds rather than blindly using a local debug/upload certificate.
Use device verification tooling such as pm get-app-links when diagnosing association.
Custom schemes are registered handlers, not verified website ownership.
iOS
For Universal Links, verify:
Associated Domains entitlement: applinks:host
+ correct signed bundle/application identity
+ apple-app-site-association
+ matching components/path policyConfigure every required host/subdomain deliberately. Do not promise that a matching Universal Link always opens the app; user/browser/platform context can keep a link on the website. Custom schemes do not provide the ownership guarantees of Universal Links.
macOS / Windows / Linux / Web
Read the platform reference before implementation. Do not copy mobile setup verbatim:
- macOS Universal Link forwarding has platform-specific integration.
- Windows protocol/web-to-app activation depends on package/registration model.
- Linux package docs emphasize custom-scheme desktop registration rather than a universal verified-HTTPS model.
- Web should normally use Router/browser URL handling; do not model
app_linksWeb as a mobile-style runtime stream.
Step 5 — Own delivery once
Prefer one app-scoped source/coordinator:
AppLinks
→ one subscription
→ one parser/validator
→ typed intent
→ one coordinator
→ one navigation authorityDo not subscribe in build(). Do not create independent global listeners for auth, analytics, and navigation.
AppLinks() being singleton-backed does not make multiple subscriptions safe; each listener can still process the same event.
Cancel subscriptions owned by finite-lifetime objects.
Step 6 — Choose startup semantics deliberately
The current package exposes both:
getInitialLink()
uriLinkStreamand the runtime stream can include the initial event according to package/platform behavior. Choose one strategy:
- stream-first for initial + runtime delivery, or
- explicit initial-link orchestration plus runtime stream
If using both, route both through the same handler and deduplicate. Do not rely on subtle plugin timing as the only double-navigation defense.
Use
getLatestLink()only for an explicit latest-link/diagnostic requirement; never poll it as a substitute for the stream.
Step 7 — Parse and validate external input
Use Uri or Uri.tryParse.
Reject before route resolution if any required property fails:
- scheme allowlist
- exact host allowlist
- expected port policy
- route/path grammar
- parameter presence/type/length
- repeated-query rule
- redirect allowlist
- protocol-specific token shape Never validate domains with:
startsWith("https://example.com")
host.contains("example.com")For subdomains, explicitly decide whether all descendants of a trusted suffix are actually trusted. Normalize only semantics known to be equivalent. Do not alter signed URLs or case-sensitive/path-sensitive values casually.
Step 8 — Convert to typed intents
For a non-trivial application, cross the trust boundary once:
untrusted Uri
→ validation/parser
→ DeepLinkIntentExamples:
OpenProduct(productId)
OpenBilling
AcceptInvite(token)
ResetPassword(token)Keep raw protocol details out of repositories/widgets/business services after this point unless the protocol specifically requires the original value. Use explicit stable intent categories instead of substring route chains.
Step 9 — Handle readiness, auth, and authorization
A link can arrive before:
- dependency injection
- auth restoration
- tenant/account state
- migration/update gates
- router readiness Use a bounded pending-intent model:
validated typed intent
→ readiness gate
→ auth gate
→ authorization/resource validation
→ consume once
→ navigateFor auth-gated links:
- validate before storing
- store typed intent, not arbitrary redirect URL
- bound lifetime/queue size
- authenticate
- reload current account/tenant state
- revalidate intent
- authorize resource/action
- clear/consume exactly once
- navigate
Clear or invalidate pending intents on logout/account switch when appropriate.
Never let
?next=become an unrestricted route or external redirect. Prefer a small allowlisted internal destination identifier.
Step 10 — Deduplicate and make actions idempotent
Duplicate delivery can arise from:
- initial method + stream
- multiple subscriptions
- Router +
app_links - notification + link
- repeated taps
- lifecycle timing
- application retry Use a short-window canonical-URI deduplicator for navigation duplication when useful. For high-value actions, backend/domain idempotency is separate and mandatory. Bad:
link callback → POST accept-invite/payment/deleteBetter:
link → validated screen/intent
→ backend loads authoritative state
→ authenticated/authorized/idempotent operationStep 11 — Route through one authority
If using go_router deliberately after app_links intake:
typed intent
→ explicit internal location
→ router.go()/push()Do not call router.go(uri.toString()) on arbitrary external input.
If Router/go_router already owns platform deep links, avoid adding the second app_links path.
For Navigator apps, depend on a root navigator abstraction/key rather than storing a long-lived BuildContext in the deep-link service.
Step 12 — Protect sensitive links
Treat reset, magic-login, invitation, email-verification, OAuth, and payment-related URLs as credentials or security-sensitive protocol input. Do not log complete URLs containing secrets. The backend must enforce applicable:
- expiration
- one-time use
- intended account
- transaction/resource state
- authorization
- replay/idempotency Prefer verified HTTPS flows for sensitive public links when practical. Custom-scheme uniqueness is not a security guarantee. Do not execute destructive actions solely because the OS delivered a link.
Step 13 — Add privacy-safe observability
Log semantic outcomes, not secret-bearing URLs:
deep_link_received type=product
deep_link_rejected reason=unsupported_host
deep_link_deferred reason=auth_required
deep_link_navigation_completed destination=productUseful diagnostic layers:
- platform eligibility/association
- native delivery
- listener started
- parser result
- duplicate suppression
- readiness/auth deferral
- authorization failure
- navigation completion Keep platform delivery failure distinct from application route failure.
Step 14 — Test in layers
Parser tests
Cover:
- valid routes
- unsupported scheme
- exact-host/look-alike host attacks
- unsupported path
- missing/oversized parameters
- percent encoding
- Unicode
- unexpected port
- trailing-slash semantics
- repeated query keys
- fragments
- external redirects
Coordinator tests
Use a fake DeepLinkSource rather than platform channels.
Verify:
- initial-link handling
- runtime stream handling
- deduplication
- readiness
- auth deferral
- pending intent consumption
- logout/account switch
- reporter/navigation behavior
Platform tests
Test actual terminated and running application flows. Android:
- normal external tap
- association state
- production/release signing
- browser fallback iOS:
- production Associated Domains/AASA
- real-device Universal Link behavior
- custom scheme where used Desktop/Web:
- actual registration/browser-routing behavior for the selected deployment model A parser unit test is not proof that OS association works.
Step 15 — Debug by layer
When a link does not work, locate the failing layer:
OS considers app eligible?
→ registration/association valid?
→ competing Flutter/plugin owner?
→ native app_links receives event?
→ Dart listener active?
→ parser accepts URI?
→ readiness/auth/authorization passes?
→ router navigates?If an Android HTTPS link opens the browser, inspect App Link verification before changing Dart routes.
If an iOS Universal Link stays in Safari, inspect Associated Domains/AASA and browser/user context before blaming app_links.
If a link is received twice, solve ownership first, deduplication second.
Migration and refactoring rules
When changing an existing deep-link system:
- map the full existing delivery path
- inventory published URL contracts
- add characterization tests
- choose one delivery owner
- centralize validation
- introduce typed intents where complexity warrants it
- preserve old public routes during migration when needed
- update association files and web fallbacks
- test release association
- monitor old-route usage before removal
Do not regenerate native platform projects or replace the router merely to adopt
app_links.
Review output
When reviewing a repository, report:
Deep-link scope
SDK/package/router versions inspected
Target platforms
Current delivery owner(s)
Public URI contracts
Platform association status
Security/trust-boundary findings
Duplicate-delivery risks
Readiness/auth/authorization flow
Pending-intent behavior
Routing integration
Logging/privacy findings
Tests present/missing
Production-association evidence
Recommended changes in priority order
Residual risks / unverified assumptionsFor each material finding use:
### [Severity] Finding
- Evidence:
- Failing layer:
- Impact:
- Recommended fix:
- Verification:Production gate
Do not call the implementation production-ready until applicable conditions are evidenced:
- one authoritative delivery pipeline exists
- locked package APIs match implementation
- initial/runtime behavior is intentional
- competing Router/plugin handling is disabled or intentionally coordinated
- all external URIs cross scheme/host/path/parameter validation
- typed intent boundary exists where complexity warrants it
- readiness is explicit
- auth-gated destinations are preserved safely
- pending intents are revalidated
- authorization is independent from authentication
- duplicate delivery is harmless
- high-value actions are idempotent
- sensitive URL data is redacted
- Android production association and signing are verified
- Apple production association/signing are verified
- targeted desktop/Web behavior is tested
- cold-start and runtime tests pass
- malicious URI tests pass
- user-facing invalid/expired/unauthorized states are intentional
- public URI compatibility/migration behavior is understood If an important production association, signing, authorization, or security condition is not verified, say so explicitly rather than claiming success.
Final operating rule
Start every deep-link task with:
Who owns platform delivery?
→ Does app_links need to exist?
→ Which URI origins and routes are accepted?
→ Is OS/domain association verified?
→ How is raw input converted into a typed intent?
→ Is the app ready?
→ Is authentication required?
→ Is the user authorized?
→ Could this event already have been handled?
→ Which single router owns navigation?
→ How will failure be tested and observed?Then implement the smallest architecture that preserves those guarantees.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

