RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-06-09Intermediate

Deep Links in Rork Max — Universal Links and URL Schemes

A hands-on guide to deep linking in Rork Max apps: when to use URL Schemes vs. Universal Links, the AASA/assetlinks pitfalls, and the cold-start trap — with working examples.

rork58deep-linksuniversal-linksreact-native12tutorial20

Carry the user to the right screen in one tap

You send a push notification, the user taps it — and lands on the home screen. The message said "Today's wallpaper is ready," but the wallpaper itself is still several taps away, and most people give up before they get there. Running wallpaper apps, this was a leak I watched happen far too often.

Deep links close that distance to zero. Tapping a URL like myapp://wallpaper/sky-blue launches the app and opens that wallpaper's detail screen directly. Whether the entry point is a notification, a website, another app, or a tracked marketing link, a deep link carries the user straight to the one screen you meant.

This guide walks through implementing both URL Schemes and Universal Links in a Rork Max (Expo-based) app, and the snags you only discover on a real device.

URL Schemes vs. Universal Links — which, and when

The two methods play different roles. A URL Scheme (myapp://path) is the easiest to set up and is ideal for in-app testing and inter-app calls. The catch: if the app isn't installed, the tap does nothing, and to the user the link simply looks broken.

Universal Links (iOS) and App Links (Android) both use ordinary https://myapp.com/path web URLs. If the app is installed, the URL opens the app; if not, it opens the web page instead. That seamless fallback is the whole point, and it's the approach Apple officially recommends. For any URL you hand to users — notifications, campaigns — I default to Universal Links / App Links.

In practice the split is simple: use URL Schemes for internal flows and debugging, and Universal Links for anything a user will tap. Keep both wired up and you rarely have to think about it.

Declare your scheme and associated domains in app.config

Projects generated by Rork Max are Expo-based, so you declare your entry points in app.json (or app.config.ts).

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "associatedDomains": ["applinks:myapp.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            { "scheme": "https", "host": "myapp.com", "pathPrefix": "/wallpaper" }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

scheme registers the URL Scheme (myapp://), while associatedDomains and intentFilters declare your Universal Links / App Links. So far this is all config-file work, and almost nobody trips here. The real problems live on the server side.

Where to look first when Universal Links "don't work"

"I added every setting, but tapping the link still opens Safari." That's the most common Universal Links symptom, and the cause is almost always the server-side verification file, not your app code.

iOS fetches a file at https://myapp.com/.well-known/apple-app-site-association (the AASA) for each declared domain, and only opens the app once the contents match. Three failures dominate: the file isn't served under /.well-known/, the Content-Type isn't application/json, or there's a redirect (301/302) in the way. Any one of those and iOS quietly gives up and opens the URL as a plain web link in Safari.

A correct AASA returns this JSON with a clean 200 and no redirect:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["YOUR_TEAM_ID.com.example.myapp"],
        "components": [
          { "/": "/wallpaper/*", "comment": "links into wallpaper detail" }
        ]
      }
    ]
  }
}

Android reads https://myapp.com/.well-known/assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
    }
  }
]

The line that caught me out was sha256_cert_fingerprints. If you ship a production AAB while leaving your local debug certificate's fingerprint in place, it won't match the Play signing key and App Links never verify. For production, use the SHA-256 shown under "App signing" in the Play Console — an unglamorous detail that matters. After deploying either file, I always open its URL in a browser to confirm it returns raw JSON with no redirect.

React Navigation integration and the cold-start trap

Once the entry points resolve, you map URLs to screens. You can ask Rork Max in plain language:

"Add these deep links to the app:
- myapp://home → Home screen
- myapp://wallpaper/:id → Wallpaper Detail screen
- myapp://profile → Profile screen
Implement it using React Navigation's linking config."

The generated linking config looks roughly like this:

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: 'home',
      WallpaperDetail: 'wallpaper/:id',
      Profile: 'profile',
    },
  },
};
 
<NavigationContainer linking={linking}>
  {/* ... */}
</NavigationContainer>

Here's the trap you only feel on a device. There are two paths for receiving a link: when the app is already running (warm), Linking.addEventListener('url', ...) fires; but when the app launches from a fully terminated (cold) state, you'll miss the first URL unless you also read it with Linking.getInitialURL().

That's exactly what bit me in a wallpaper app. I'd written only addEventListener by hand, so tapping the notification while the app was killed landed on the home screen every time — never the wallpaper screen I intended.

// Before: only catches warm launches, misses cold start
import * as Linking from 'expo-linking';
 
useEffect(() => {
  const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
  return () => sub.remove();
}, []);
// After: explicitly read the launch URL too
import * as Linking from 'expo-linking';
 
useEffect(() => {
  // Cold start: the URL the app was opened with
  Linking.getInitialURL().then((url) => {
    if (url) handleUrl(url);
  });
  // Warm: navigation while running
  const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
  return () => sub.remove();
}, []);

The good news: if you use React Navigation's linking config, it reads getInitialURL internally, so both paths are handled for you. The flip side — you only need to catch the cold start yourself when you bypass the linking config and handle URLs manually. Keep that distinction in mind and the confusion disappears.

Route from a push notification to a specific screen

Finally, let's fix the "tap the notification, land on home" problem from the opening. Put the deep link URL in the notification's data payload, then open it on tap.

// Server side: include the destination URL in the notification data
const notification = {
  title: "Today's wallpaper is ready",
  body: "Added a new sky gradient",
  data: { url: "myapp://wallpaper/sky-blue" }
};
 
// App side: open the URL on tap
import * as Notifications from 'expo-notifications';
 
Notifications.addNotificationResponseReceivedListener((response) => {
  const url = response.notification.request.content.data?.url;
  if (url) Linking.openURL(url);
});

Calling Linking.openURL hands the URL to the linking config you defined, which carries it all the way to the wallpaper detail screen. After years of running small apps, I've come to believe that where the tap lands matters more for retention than the open rate itself.

Your next step

Start with just the URL Scheme: run npx uri-scheme open "myapp://wallpaper/sky-blue" --ios from your terminal against the simulator and confirm it lands on the screen you intended. Once that path works, wiring up AASA / assetlinks for Universal Links is a short, continuous step from there.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-24
Why your Rork app shows a blank screen or loses state after returning from background
Your Rork app sits in the background overnight, you tap the icon the next morning, and the screen is blank — or your drafts have disappeared, or you have been silently logged out. iOS process reclamation, AsyncStorage rehydration timing, and Navigation state restoration each cause a different version of this bug. Notes from running wallpaper and wellness apps with 50M downloads as an indie developer.
Dev Tools2026-05-23
Rork-Specific 'expo start --offline forbidden': Four Causes in Rork's Template Config
When expo start --offline returns 'forbidden' specifically on a Rork-generated project, the cause is usually Rork template config: tsconfigPaths, an un-generated expo-router cache, native prebuild, or a lockfile mismatch. Four Rork-specific fixes; the generic Expo proxy and dependency-validation guide is covered separately.
Dev Tools2026-05-20
Fixing 0x8badf00d Watchdog Kills That Wipe Out Rork Apps at Launch
Your Rork iOS app dies right after launch on real devices, but never on your bench. Crashlytics shows exception code 0x8badf00d. Here is how to read the exact watchdog budget out of the crash log and rearrange your launch path so it stops.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →