For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Deep linking

Open auth, invites, and dashboard screens from custom schemes, Universal Links, App Links, and push notification taps.

Deep links take users from email, push notifications, or the web into a specific screen in your Expo app. TurboStarter already ships a custom scheme, Expo Router routes, and auth callbacks that build app URLs with expo-linking. You add Universal Links (iOS) and App Links (Android) when you want https:// URLs on your domain to open the app instead of the browser.

Development build for production links

Custom schemes work in development builds. Universal Links and App Links require a development build (or store build) - Expo Go cannot embed your associated domains. See development builds.

Architecture

Out of the box you get:

  • Custom scheme turbostarter in apps/mobile/app.config.ts
  • Expo Router with deep linking enabled for every file-based route
  • Canonical paths in apps/mobile/src/config/paths.ts (pathsConfig)
  • Auth and invite flows that pass Linking.createURL(...) / x-url so emails open in the app
  • Better Auth trustedOrigins that allow the turbostarter:// scheme

Still on you for production HTTPS links: associatedDomains, Android intentFilters, and .well-known verification files on the web domain.

Linking flow

Incoming URLs map to Expo Router paths. Paths mirror pathsConfig:

URL pathApp route
/auth/joinpathsConfig.setup.auth.join
/auth/password/updatepathsConfig.setup.auth.updatePassword
/dashboard/*matching pathsConfig.dashboard entries
/dashboard/organization/*org dashboard routes

Two URL shapes:

  • Custom scheme - turbostarter://auth/join?invitationId=… (already configured)
  • HTTPS - https://yourdomain.com/auth/join?invitationId=… (Universal / App Links after you finish the steps below)

Auth emails prefer the scheme URL today via Linking.createURL and the x-url header. Universal Links are for marketing, shared web URLs, and any flow where you want the same https link to open the app when installed (and the website when not).

Custom scheme

The scheme is already set:

apps/mobile/app.config.ts
export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  scheme: "turbostarter",
  // ...
});

If you rename it:

  1. Update scheme in app.config.ts
  2. Update Better Auth trustedOrigins in packages/auth/src/server.ts
  3. Rebuild the native app (eas build or local prebuild) - scheme changes are not applied by OTA alone

Trusted origin (required so auth redirects are not treated as open redirects):

packages/auth/src/server.ts
trustedOrigins: [
  "chrome-extension://",
  "turbostarter://",
  "https://appleid.apple.com",
  // ...
],

See Auth & deep links for the security checklist.

Mobile clients pass an app URL so the API can build email links that open the native app:

apps/mobile/src/modules/auth/form/password/forgot.tsx
redirectTo: Linking.createURL(pathsConfig.setup.auth.updatePassword),
apps/mobile/src/modules/organization/lib/api.ts
authClient.organization.inviteMember(params, {
  headers: {
    "x-url": Linking.createURL(pathsConfig.setup.auth.join),
  },
});

On the server, getUrl() in @workspace/auth prefers the x-url (or expo-origin) header so reset, invite, and verification emails resolve to turbostarter://… instead of a web-only URL.

The auth client also sends the scheme origin on every request:

apps/mobile/src/lib/auth/index.ts
fetchOptions: {
  headers: {
    "x-client-platform": Platform.MOBILE,
    origin: Linking.createURL(""),
  },
},

Keep pathsConfig and these createURL / x-url call sites in sync when you add screens that should be reachable from email.

Two-way association: the website proves it owns the app, and the app declares the domain.

1. Associated domains

Add your production (and staging) host without https://:

apps/mobile/app.config.ts
ios: {
  bundleIdentifier: "com.turbostarter.core",
  associatedDomains: ["applinks:yourdomain.com"],
  // ...
},

Rebuild with EAS so the Associated Domains entitlement is registered. The host should match EXPO_PUBLIC_SITE_URL / appConfig.url.

2. Apple App Site Association

Serve an apple-app-site-association file (no file extension) from your web app:

apps/web/public/.well-known/apple-app-site-association
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.turbostarter.core",
        "paths": ["/auth/*", "/dashboard/*"]
      }
    ]
  }
}

Replace TEAMID with your Apple Team ID and keep the bundle ID in sync with ios.bundleIdentifier. Scope paths to routes you actually want in-app (v1: auth + dashboard is enough).

The file must be reachable over HTTPS at:

https://yourdomain.com/.well-known/apple-app-site-association

Validate with an AASA validator after deploy. iOS caches the AASA aggressively - path changes for store builds often need a new App Store version before every device refreshes.

iOS Universal Links

docs.expo.dev

1. Intent filters

apps/mobile/app.config.ts
android: {
  package: "com.turbostarter.core",
  intentFilters: [
    {
      action: "VIEW",
      autoVerify: true,
      data: [
        {
          scheme: "https",
          host: "yourdomain.com",
          pathPrefix: "/",
        },
      ],
      category: ["BROWSABLE", "DEFAULT"],
    },
  ],
  // ...
},

autoVerify: true is required for verified App Links. Narrow pathPrefix (for example /auth or /dashboard) if you do not want every path claimed by the app.

apps/web/public/.well-known/assetlinks.json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.turbostarter.core",
      "sha256_cert_fingerprints": ["XX:XX:…"]
    }
  }
]

Get the SHA-256 fingerprint from EAS:

eas credentials -p android
# Select the build profile → copy SHA256 Fingerprint

Or from Google Play Console → Release → Setup → App Signing. Include both upload and app-signing fingerprints when Play App Signing is enabled.

Serve the file as application/json over HTTPS at:

https://yourdomain.com/.well-known/assetlinks.json

Android App Links

docs.expo.dev

Push notification taps

Push payloads can carry a route in data. Wire navigation in NotificationsProvider (or a dedicated hook) when the user taps:

apps/mobile/src/lib/providers/notifications.tsx
Notifications.addNotificationResponseReceivedListener((response) => {
  const url = response.notification.request.content.data?.url;
  // router.push(url) — validate against pathsConfig / an allowlist
});

Only put non-sensitive identifiers in data. Validate the path before navigating - treat notification URLs like any other untrusted entry point (Auth & deep links).

Example payload when sending from your backend:

{
  "to": "ExponentPushToken[…]",
  "title": "Invitation",
  "body": "Join the organization",
  "data": { "url": "/auth/join?invitationId=…" }
}

See Push notifications for delivery setup.

Custom scheme

With a development or store build installed:

# iOS
npx uri-scheme open "turbostarter://dashboard" --ios

# Android
npx uri-scheme open "turbostarter://dashboard" --android

Auth paths to smoke-test:

npx uri-scheme open "turbostarter://auth/join?invitationId=test" --ios
npx uri-scheme open "turbostarter://auth/password/update" --ios

On a physical device with a build that includes associated domains / intent filters:

  1. Deploy .well-known files to the production (or staging) domain
  2. Install the matching native build
  3. Open https://yourdomain.com/auth/join from Notes / Messages (not only the browser address bar)
  4. Confirm cold start (app killed) and warm start (app backgrounded)

Android intent smoke test:

adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d "https://yourdomain.com/dashboard"

In-app browsers (some social apps) may open HTTPS links inside a webview and skip Universal / App Links - document that limitation for marketing links.

Multiple environments

Each domain needs its own AASA / assetlinks.json. If you use staging.yourdomain.com:

  • Add applinks:staging.yourdomain.com to associatedDomains
  • Add a matching Android intent filter host
  • Host .well-known on the staging host as well

Prefer separate application IDs / EAS projects for production vs preview so signing fingerprints stay isolated. See Multiple environments.

Troubleshooting

SymptomWhat to check
Scheme does nothingRebuild after changing scheme; confirm with npx uri-scheme on a device/simulator
Auth email opens browser onlyMobile request sent x-url / Linking.createURL; trustedOrigins includes your scheme
HTTPS opens site, not app (iOS)AASA valid and public; associatedDomains uses applinks:host without https; Team ID + bundle ID match; rebuild after entitlement change
HTTPS opens chooser / browser (Android)autoVerify: true; assetlinks.json fingerprint matches the installed build; wait for verification (can take a minute+)
Works in debug, fails in Play/App StoreProduction signing fingerprint / App Store AASA cache; issue a new store build after path changes
Notification tap does nothingdata.url present; response listener navigates; path allowlisted

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter