← All articles
Jun 12, 20263 min read

Deep Links That Actually Open the App: AASA, assetlinks, and the Store Fallback

  • deep linking
  • iOS
  • Android
  • universal links
  • mobile
  • North
Deep Links That Actually Open the App: AASA, assetlinks, and the Store Fallback

An invite flow sounds like a UI task: someone taps a link like north.app/join/<code>, the app opens to the join screen, family connected. Shipping it for North confirmed the ratio I posted at the time: classic — more plumbing than feature. The page took an hour. Making the link reliably open the app took the rest.

Old-style custom schemes (north://join) are unauthenticated — any app can claim a scheme, which is why both platforms moved to verified HTTPS links: the website must vouch for the app.

iOS — Apple App Site Association. A JSON file served at https://<domain>/.well-known/apple-app-site-association declares which app ID may handle which paths (/join/*). The traps are all operational: it must be served as application/json without a redirect; iOS fetches it via Apple's CDN at app install time, not at tap time, so a broken AASA isn't fixed by fixing the file — users need a reinstall/update cycle before verification re-runs. Debugging this with "change file, tap link, nothing happens" teaches you patience you didn't want.

Android — assetlinks.json. Same idea at /.well-known/assetlinks.json, declaring the package name and SHA-256 signing-certificate fingerprint. The classic failure: verifying against your upload key's fingerprint while Play App Signing actually signs releases with Google's key — links verify in internal testing, then silently fail in production. The fingerprint you publish must be the one from the Play Console, and if you have debug, internal, and production builds, you list them all.

Both files are infrastructure with no UI and total veto power: if they're wrong, every invite link in every chat app on earth opens a browser instead of your app.

The fallback is the actual product

Verification only matters for people who have the app. Invite links go disproportionately to people who don't — that's what an invite is. So the /join web page is the real flow, and it has to do three jobs:

  1. Minimal disclosure. The page reveals only that an invite exists — not the circle name or members. An invite code in a URL travels through chat previews, screenshots, and link scanners; the page assumes the viewer might be none of your business.
  2. Route to the right store. Platform-detect and hand off to the App Store or Play Store. Pedestrian, but it's the difference between "tap → install → done" and a dead end.
  3. Survive the install gap. The brutal part: the user taps the invite, installs the app, opens it — and the original URL is gone. iOS offers no reliable deferred deep linking without third-party SDKs. North's pragmatic answer: keep the code visible and copyable on the /join page ("after installing, enter this code"), so the human carries the payload across the gap the platform won't bridge. Deferred-attribution SDKs can automate this; for a privacy-positioned app, not adding an attribution SDK is part of the point.

The testing matrix nobody budgets for

The reason this eats days is combinatorics: {app installed, not installed} × {iOS, Android} × {tapped in Safari, Chrome, WhatsApp, Instagram's in-app browser, a QR code}. In-app browsers are the gremlins — several open universal links in their own webview instead of the app, so the /join page also needs an "Open in app" affordance for the cases where verification was fine and the host app simply refused.

The lesson generalizes past invites: a deep link is a distributed contract between your website, two app stores, signing infrastructure, and every chat app's link preview. Write the two JSON files first, with the production fingerprints, before building anything that depends on them — and treat the no-app web page as the feature, not the fallback.

WRITTEN BY

Shahzaib Muhammad Akram

Senior Frontend EngineerCyberjaya, Malaysia