Deep Links vs Universal Links Behind a QR Code — featured illustration
Technology

Deep Links vs Universal Links Behind a QR Code

6 min read
Create a QR code

Scan a code on a bus shelter and one of three things happens. The app opens on exactly the right screen. A browser opens the right page. Or you get an error saying the address is invalid, and the poster has just wasted its one chance at you.

Which of the three you get has almost nothing to do with the QR code and almost everything to do with the URL printed inside it.

The code only carries a URL

Worth settling first: a QR symbol encodes text. When that text starts with a recognized scheme, the scanning app treats it as a link and hands it to the operating system. There is no field for "open my app", no flag for "prefer native", no place to store a fallback. Everything below is a decision about the destination, made before you generate the code and changeable afterward only if the code points at a redirect you control.

That last point is the practical argument for putting a dynamic code on anything printed. App association details change. Domains move. A static code freezes the URL into ink, and if the app handoff was set up wrong, the only fix is a reprint.

Custom schemes fail quietly

The oldest way to reach an app is a private scheme: an address beginning with something like myapp, followed by the usual colon and slashes. The app declares the scheme, the OS routes matching URLs to it, and the app parses the rest.

It works when the app is installed. When it is not, the failure is bad in a specific way. There is no website at that address, because the address is not a web address at all. Depending on the browser and the OS version you get an error page, a silent no-op, or a dialog offering to search for it instead. Behavior varies between platforms and even between releases of the same platform, which should keep custom schemes off printed material.

Two more weaknesses. Scheme names are not registered anywhere and not verified, so any app can claim any scheme, and what happens when two apps claim the same one is not defined by either platform. And because a scheme carries no domain, neither platform can tie it to a site you have proven you control.

Custom schemes still have a legitimate home: internal navigation, a handoff between two apps you own, a redirect target reached from your own page after you have already confirmed the app is present. Not the front door.

Universal Links and App Links are ordinary https URLs

Both platforms solved the same problem the same way. Rather than inventing a new address space, they let an app claim a set of ordinary https URLs belonging to a domain the developer can prove they control.

Apple calls its version Universal Links. Google calls its version Android App Links. In both cases the address in your QR code is a normal web address, the sort you could type into any browser. When the app is installed and the claim has been verified, the OS opens the app instead of the browser. When the app is missing, nothing special happens and the page loads as a web page. That graceful degradation is the entire reason this shape is the correct one behind a printed code.

The association files

Verification happens through a file on your web server.

  • iOS looks for a JSON file named apple-app-site-association, served over https from the well-known directory at your domain root, listing the app identifiers and the URL paths each one claims. The app side needs the Associated Domains capability naming the same domain. Apple fetches the file through its own infrastructure, so changes do not always take effect immediately.
  • Android looks for assetlinks.json in the same well-known directory. It lists the app's package name and the SHA-256 fingerprint of the certificate the app was signed with, which is what ties the claim to a specific build rather than to anything that merely says it is that package. The app's manifest declares an intent filter for the domain with automatic verification switched on.

Common ways this breaks: the file served through a redirect, a CDN rule that changes its content type, a stale certificate fingerprint after moving to platform-managed app signing, or a path pattern that does not cover the URL your code actually points at. All of them fail the same way from the outside, with the link opening in a browser and no error visible anywhere.

The fingerprint mismatch deserves particular attention because it is silent and build-specific. The fingerprint has to match the certificate of the build the user installed, not the one on your laptop.

The scanner in the middle

There is a step between the camera and the operating system, and it is not consistent.

A platform camera app that hands the URL to the system gives the association mechanism its best chance to fire. A scanner built into a social or messaging app often opens the link inside its own embedded browser instead, and embedded browsers do not reliably hand off to a native app. Whether your Universal Link opens the app therefore depends partly on which scanner the person happened to use. Some platforms also remember a user's earlier choice to stay in the browser for a given domain and honor it on later visits.

Design as though the handoff might not fire, because sometimes it will not. If the web page at that URL shows the same content the app screen would have shown, a missed handoff is a mild disappointment rather than a dead end.

The fallback chain worth building

Three destinations, in order:

  1. The app, on the right screen. Handled by the OS through the https URL, with no JavaScript involved.
  2. The web page, with the same content. The same URL, rendered for a browser. This is the layer that catches every person without the app and every scanner that swallowed the handoff.
  3. The store, offered rather than forced. Put a clear install link on the page. Redirecting straight to a store listing is the mistake, because it strands everyone who wanted the content and did not want a download.

Skip the old timer trick of firing a custom scheme from JavaScript and redirecting to a store if nothing happens within a second or two. It fights browser popup rules, misfires on slow devices, and behaves differently across OS versions. Chrome on Android does support an intent URL form with a declared browser fallback, which is genuinely useful, though it is specific to that browser rather than something to depend on everywhere.

Test the printed proof on a device with the app removed, then on the same device with the app installed, then through at least one third-party scanner. If the first test lands on a working page rather than an error, most of the value is already banked. When a code reaches the wrong place entirely, work through the scan failure checklist before blaming the association file, and keep in mind that the symbol itself is doing nothing more than handing over a string.

Share this article