Deep links that open the right screen: production checklist for Expo apps A developer's production checklist for Expo apps addresses deep-link failures that occur when apps are killed, sessions expire, or links are pasted elsewhere. The guide identifies four common bug buckets—OS association, route mapping, cold-start handling, and unauthenticated access—and recommends universal links over custom schemes for production, with Expo Router enabling automatic deep linking for file routes. Password-reset emails, referral invites, and order confirmations all end the same way: a link that should land on the right screen. In development the link works because the simulator is warm, the session is fresh, and you tap it from the same device. In production it breaks because the app was killed, the session expired, or the user pasted the link into a notes app first. Deep linking is not one feature. It is three layers that have to agree: the operating system deciding your app owns the URL, the router mapping that URL to a screen, and your app handling whatever state it wakes up in. When AI coding tools scaffold the router for you, the middle layer looks done while the other two are still missing. This post closes that gap for Expo apps built with Expo Router. Most production deep-link bugs fall into four buckets. First, the OS never hands the URL to your app because the domain association file is missing or the native config is wrong, so the link opens in the browser instead. Second, the app opens but lands on the home screen because the route path does not match the URL structure. Third, the app lands correctly on a warm start but drops the destination on a cold start, when the JavaScript bundle loads after the OS delivers the intent. Fourth, the destination screen assumes an authenticated user, redirects to login, then forgets where it was going. Each bucket needs a different fix, which is why retesting the happy path never resolves production reports. You need the OS association verified, the route mapping explicit, and the cold-start plus unauthenticated paths handled as first-class cases. The checklist below walks through all three in the order that fails fastest. If your auth layer is still shaky, fix that alongside linking. The guard pattern in our Expo Router auth guards guide https://dev.to/blog/expo-router-auth-guards-production pairs well with this post because every protected deep link eventually meets an expired session. A custom scheme such as otfkit://orders/123 is easy to configure and fine for local testing. It is a poor default for production because any app can claim the same scheme, there is no ownership proof, and messaging apps and email clients handle custom schemes inconsistently. Users who long-press, preview, or paste the link often end up somewhere unexpected. Universal linking solves ownership. On Android, App Links bind an https URL to your app through an intent-filter plus a hosted assetlinks.json file. On iOS, Universal Links bind an https URL through an associatedDomains entitlement plus a hosted apple-app-site-association file. When both sides match, the OS opens your app directly with no disambiguation dialog. When the app is not installed, the same URL falls back to your website, which is exactly the behavior a referral or receipt link should have. The practical rule is simple: keep the custom scheme as a development convenience, ship universal links as the real path, and make both resolve to the same routes. That way QA can test with the scheme while users only ever see https links. Expo Router enables deep linking for every file route automatically, which removes a whole class of manual mapping bugs. Your job is to keep the URL structure stable and to avoid overriding the default behavior unless you have a concrete reason. Start by defining the canonical URL shape in app.json before adding screens. One scheme for development, one web domain for production, with the native association declared in the platform sections covered below. { "expo": { "scheme": "otfkit", "extra": { "webDomain": "https://otf-kit.dev" } } } Map each linkable destination to a file route and keep the segments identical to the web path. A receipt at https://otf-kit.dev/blog/expo-sqlite-offline-cache-apps resolves through the same segment structure as the screen file, not through a differently named screen plus a manual redirect. The fewer translations between URL and route, the fewer places a cold start can lose the parameter. js // app/orders/ id .tsx import { useLocalSearchParams } from 'expo-router'; import { Text, View } from 'react-native'; export default function OrderScreen { const { id } = useLocalSearchParams<{ id: string } ; if id return