# Deep Links Universal Links

> Implementing and testing deep links on mobile - Android App Links, iOS Universal Links, custom URL schemes, attribution providers, and sunset guidance for Firebase Dynamic Links. Use when adding, debugging, or migrating deep link behavior.

- Skill: `almasumdev/deep-links-universal-links` (Agent Skill)
- Install (CLI): `npx skillmds@latest add almasumdev/deep-links-universal-links`
- Raw SKILL.md: https://api.skillmd.com/api/skills/almasumdev/deep-links-universal-links/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: almasumdev (https://skillmd.com/u/almasumdev)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/almasumdev/deep-links-universal-links

---


# Deep Links and Universal Links

## Instructions

Deep links are the front door of a mobile app. They power marketing, referrals, support links, push notifications, and share flows. Get them wrong and none of the growth loops work.

### 1. Link Types

| Type | iOS | Android | Properties |
| --- | --- | --- | --- |
| Custom scheme | `myapp://` | `myapp://` | Works without verification, not shareable on the web |
| Universal/App Link | `https://app.example.com/...` | `https://app.example.com/...` | Verified domain, opens app if installed, falls back to web |
| Deferred deep link | 3rd-party SDK | 3rd-party SDK | Carries context through install from store |
| Firebase Dynamic Links | deprecated | deprecated | **Sunset Aug 25, 2025**. Do not build new integrations. |
| Branch / Adjust / AppsFlyer | Supported | Supported | Replacement for FDL, attribution included |

### 2. iOS Universal Links

1. Host an `apple-app-site-association` (AASA) JSON file at `https://example.com/.well-known/apple-app-site-association`, served over HTTPS, no redirects, content-type `application/json`.
2. Add the `Associated Domains` entitlement with `applinks:example.com`.
3. Handle the link in `SceneDelegate` / SwiftUI.

```json
{
  "applinks": {
    "details": [{
      "appIDs": ["TEAMID.com.example.app"],
      "components": [
        { "/": "/article/*" },
        { "/": "/u/*" }
      ]
    }]
  }
}
```

```swift
// SwiftUI
ContentView()
  .onOpenURL { url in DeepLinkRouter.shared.handle(url) }
  .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
      if let url = activity.webpageURL { DeepLinkRouter.shared.handle(url) }
  }
```

### 3. Android App Links

1. Declare an intent filter with `android:autoVerify="true"`.
2. Host `assetlinks.json` at `https://example.com/.well-known/assetlinks.json`.
3. Verify with `adb shell pm get-app-links com.example.app`.

```xml
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity" 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" />
    <data android:pathPrefix="/article/" />
  </intent-filter>
</activity>
```

```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["AB:CD:..."]
  }
}]
```

### 4. Cross-Platform Handling

```dart
// Flutter via go_router
final router = GoRouter(routes: [
  GoRoute(path: '/article/:id', builder: (_, s) => ArticleScreen(id: s.pathParameters['id']!)),
]);
// plus app_links plugin to catch initial and streamed URIs
```

```tsx
// React Native with Expo Linking
Linking.getInitialURL().then((url) => url && router.handle(url));
Linking.addEventListener('url', ({ url }) => router.handle(url));
```

### 5. Attribution and Deferred Deep Links

If a link must carry context **through a fresh install from the store**, the platforms alone do not solve it. Options:

- **Branch**, **AppsFlyer OneLink**, **Adjust** deep links -- full attribution and deferred deep linking.
- Build your own with a clickthrough page that writes a short-lived token to a server and reads it on first launch (by IP+UA fingerprint or a one-time code flow).

Firebase Dynamic Links is sunsetted (August 25, 2025). Migrate to one of the above or roll your own.

### 6. Routing Contract

All deep links must resolve through a single router:

- Parse the URL into a typed route.
- Reject unknown paths gracefully (land on a sensible screen, log a metric).
- Require auth where appropriate; store the target URL and replay after sign-in.
- Establish a back stack so the system back returns to a reasonable place.

```kotlin
sealed class Route {
    data class Article(val id: String) : Route()
    data class Profile(val handle: String) : Route()
    data object Unknown : Route()
}
fun parse(uri: Uri): Route = when {
    uri.pathSegments.firstOrNull() == "article" && uri.pathSegments.size == 2 ->
        Route.Article(uri.pathSegments[1])
    uri.pathSegments.firstOrNull() == "u" && uri.pathSegments.size == 2 ->
        Route.Profile(uri.pathSegments[1])
    else -> Route.Unknown
}
```

### 7. Testing

- **iOS**: `xcrun simctl openurl booted https://app.example.com/article/42`.
- **Android**: `adb shell am start -W -a android.intent.action.VIEW -d "https://app.example.com/article/42" com.example.app`.
- **Custom scheme iOS**: `xcrun simctl openurl booted myapp://article/42`.
- **Custom scheme Android**: `adb shell am start -a android.intent.action.VIEW -d "myapp://article/42"`.
- Always test both cold start and warm start paths. They behave differently.

### 8. Common Bugs

- AASA not served with the right content-type or blocked by redirects. Verify with `curl -I`.
- `autoVerify` failing due to missing `assetlinks.json` for the release keystore. Both debug and release SHA-256s must be present.
- Links opening the browser instead of the app because the user explicitly disassociated the domain. Provide a manual "open in app" fallback.
- Universal Links failing inside apps that use in-app browsers (WKWebView, SFSafariViewController navigations). Tap-through from Safari to the app is fine; programmatic opens often are not.
- Links arriving before the router is mounted on cold start. Buffer and replay when the navigator is ready.

### 9. Anti-Patterns

- Using custom schemes for marketing links (unshareable, unverified).
- Embedding user secrets in link paths or query strings.
- Relying on Firebase Dynamic Links for new projects.
- Not versioning the link namespace. Reserve a path prefix like `/l/v1/` so breaking changes are possible.

## Checklist

- [ ] AASA and `assetlinks.json` are hosted correctly and verified in CI.
- [ ] Associated Domains entitlement and App Links intent filter are present.
- [ ] A single router parses and dispatches every URL.
- [ ] Unknown links degrade gracefully.
- [ ] Cold-start links are buffered and replayed after navigator is ready.
- [ ] Auth-gated routes store and replay the target URL.
- [ ] Attribution/deferred deep linking uses a supported provider (not Firebase Dynamic Links).
- [ ] Both debug and release signing SHA-256s are in `assetlinks.json`.
- [ ] Manual QA steps (`simctl`, `adb`) are documented.

