With Expo Router, how do habits://waitlist/confirm and https://example.com/waitlist/confirm open the right screen without any linking config?
answer
- file path is the URL
- no linking object to write
- scheme host becomes first segment
- https: origin stripped, path kept
- +native-intent rewrites odd links
basics
~20 sExpo Router derives every screen's URL from its file path, so both links resolve to /waitlist/confirm and open src/app/waitlist/confirm.tsx. For a custom scheme the host counts as the first path segment; for HTTPS the origin is stripped.
solid answer
~40 sExpo Router generates the linking configuration from the files under `src/app`, so every screen is deep-linkable as soon as the app can receive URLs. For a custom-scheme link such as `habits://waitlist/confirm`, it joins the URL's host and path, so `waitlist` is the first segment and the route is `/waitlist/confirm`. For a verified HTTPS link, universal links on iOS and app links on Android, it strips the origin and keeps the path, which is the same path the web build serves. So one URL opens the app screen or the web page. Groups do not appear in paths, query strings become params, unmatched paths show `+not-found`, and non-conforming third-party links can be rewritten in `+native-intent.tsx`.
code
typescript · 13 lines// src/app/+native-intent.tsx
export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
try {
// path may be a full URL (habits://join?ref=x, https://example.com/join) or a path
const legacy = path.match(/^(?:habits:\/\/|https:\/\/example\.com\/|\/)?join(\?.*)?$/);
if (legacy) {
return '/waitlist' + (legacy[1] ?? '');
}
return path;
} catch {
return '/';
}
}go deeper
Recall that every Expo Router screen is deep-linkable automatically and that its URL is its file path.
Explain how custom-scheme and HTTPS URLs are mapped to paths, including the host-as-first-segment rule, and what +not-found and +native-intent handle.
Plan for stable public paths, cold-start testing, guarded screens a link might reach, and untrusted params arriving from a URL.
Treat the file tree as a public URL contract shared by web and app, with a deprecation process for renamed or moved routes.
## Every file route is already a link In **Expo Router**, the file tree under `src/app` is the navigation config. The router derives the URL for every screen from its file path, and uses that same mapping in reverse to turn an incoming URL into navigation state. So there is no linking configuration to write: once the app has a way to receive URLs, **every screen is deep-linkable automatically**. For a habit-tracker app with `src/app/waitlist/confirm.tsx`, the path is `/waitlist/confirm` on the web, in a custom-scheme link and in an HTTPS link alike. Group folders such as `(marketing)` do not appear in URLs, and a query string arrives as route params. ## Custom-scheme links A **custom scheme** is the app's own URL prefix, set by the `scheme` field in the app config, for example `habits`. Registering it with iOS and Android happens during prebuild. If no scheme is set, prebuild registers the Android package name and iOS bundle identifier as schemes instead. Expo Router reads a custom-scheme URL by joining its **host and path**: | Incoming URL | Route path | |---|---| | `habits://waitlist/confirm` | `/waitlist/confirm` | | `habits://waitlist/confirm?ref=email` | `/waitlist/confirm` with `ref` as a param | | `habits:///waitlist/confirm` | `/waitlist/confirm` | | `habits://` | `/` | The trap is the first row: in `habits://waitlist/confirm`, `waitlist` is technically the URL's host, but Expo Router treats it as the first path segment. Developers who expect the host to be ignored build links that land on the wrong screen. In Expo Go, links use the `exp://` scheme with the path after `/--/`, for example `exp://127.0.0.1:8081/--/waitlist/confirm`. ## HTTPS links: universal links and app links An **HTTPS link** such as `https://example.com/waitlist/confirm` opens the app only after the operating system has verified that the app and the domain belong together (iOS universal links, Android app links). When it does, Expo Router **strips the origin and uses the path**, so the link resolves to exactly the route the same URL renders on the website. That symmetry is the practical benefit of one file tree: 1. A marketing email links to `https://example.com/waitlist/confirm`. 2. On a desktop, the browser loads the page from the Expo web build. 3. On a phone with the app installed, the OS hands the URL to the app, and Expo Router opens `waitlist/confirm.tsx`. 4. On a phone without the app, the browser loads the same web page. The verification files (`apple-app-site-association` and `assetlinks.json`) can sit in `public/.well-known/` of the same project; files in `public` are copied into `dist` on export, so the web deployment that serves the pages can serve them too. ## When the URL does not match a file - A path with no matching file shows the `+not-found` route. - A link from a third-party service that does not follow your route structure can be rewritten before routing: a top-level `src/app/+native-intent.tsx` exports `redirectSystemPath({ path, initial })`, which returns the path Expo Router should open. It must never throw; return a fallback route on error. - On the web there is no `+native-intent` equivalent, because the host resolves the URL before any JavaScript runs; redirects belong to the server or the root layout. ## Compared with React Navigation's `linking` prop In a React Navigation app, each screen's path is declared in a `linking` object passed to the navigation container. In Expo Router that object is generated from the files, so a new screen file is linkable the moment it exists. The flip side is that **renaming or moving a file changes its public URL**, which can break links already sent in emails. ## Production checklist - Test cold and warm starts; the app may not be running when the link arrives. - Keep public paths stable, or rewrite old ones in `+native-intent.tsx`. - Guard sensitive screens; a deep link can target any route. - Treat params from a URL as untrusted input. This is the behaviour of Expo SDK 57 with Expo Router 57.x.
- What is the risk of moving src/app/waitlist/confirm.tsx to src/app/join/confirm.tsx?The screen's public URL changes with the file, because Expo Router derives links from paths. Links already sent in emails or shared on the web now hit `+not-found`, on both the website and the app. Keep public paths stable, or catch old paths in `+native-intent.tsx` for native links and with a server redirect for the web.
- Why is there no web equivalent of +native-intent.tsx in Expo Router?On the web, the host resolves a URL before any of the app's JavaScript runs: it serves a static file, rewrites to the SPA shell, or hands the request to the server. So rewrites for old or third-party web URLs belong in the host's redirects or server middleware, or in client code in the root layout, not in a native-only hook.
saying these in an interview costs you the question
- Expo Router needs a linking object listing each screen
- In habits://waitlist/confirm the host waitlist is ignored
- Group folders like (marketing) appear in the URL
- An https link opens the app without any domain verification
- Renaming a route file never affects existing links