Fetching latest headlines…
Opening Web Invite Links Directly in the App with Expo Router
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’August 5, 2026

Opening Web Invite Links Directly in the App with Expo Router

0 views0 likes0 comments
Originally published byDev.to

This article is an English translation of the original Japanese article.

In my club management app, I use the following invite URL for both web and iOS app:

https://squad-note.com/invite/{orgId}

If the app is installed, Expo Router opens the invite screen in the app. If not, the web page displays. Using Universal Links lets me share a single URL rather than splitting it into web and app versions.

Expo Router File Structure

I place the invite screen as a dynamic route.

apps/mobile/src/app/invite/[orgId]/index.tsx

The screen retrieves the orgId from the URL via useLocalSearchParams.

import { useLocalSearchParams, useRouter } from "expo-router";

export default function InviteScreen() {
  const { orgId } = useLocalSearchParams<{ orgId: string }>();
  const router = useRouter();

  const { data: org, isLoading } = api.organization.getPublic.useQuery(
    { id: orgId! },
    { enabled: !!orgId },
  );

  // Display invite content and execute join process
}

When opened with /invite/abc, orgId receives abc. I also provide a page with the same path on the web side.

Setting a Custom Scheme

To handle app-specific URLs, I set a scheme in the Expo config.

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  scheme: "squadnote",
});

This allows handling URLs like the following during development and authentication callbacks:

squadnote://invite/abc

However, I use HTTPS for the invite URLs shared with users. Because custom schemes can be declared by different apps with the same scheme, I use Universal Links as the entry point to securely associate normal web URLs with the app.

iOS Associated Domains

In app.config.ts, I separate domains for production and development.

ios: {
  bundleIdentifier: IS_PROD
    ? "com.squadnote.app"
    : "com.squadnote.app.dev",
  associatedDomains: IS_PROD
    ? ["applinks:squad-note.com"]
    : ["applinks:dev.squad-note.com"],
}

Adding the configuration alone does not make it work. I also serve apple-app-site-association from the web domain.

https://squad-note.com/.well-known/apple-app-site-association

On the Next.js side, I placed it as a Route Handler.

apps/web/src/app/.well-known/apple-app-site-association/route.ts

The response defines the appID combining the Apple Developer Team ID and Bundle ID, and the paths the app should open. To target only the invite link, I can narrow the range to /invite/*.

Shared URL

On the mobile side, I also share the HTTPS URL.

const inviteUrl = `${config.apiUrl}/invite/${id}?openExternalBrowser=1`;

await Share.share({
  message: `Invitation to ${orgName}\n${inviteUrl}`,
});

On iOS devices with the app installed, it opens the app as a Universal Link. Otherwise, it opens the web invite page.

openExternalBrowser=1 is a parameter used on the web side for LINE's in-app browser. It is a separate issue from the Universal Links configuration, so I treat app association and web browser workarounds as separate concerns.

Verification Considerations

Universal Links work with the combination of an installed app, the domain's AASA, and a signed build. Using only Expo Go does not provide the same verification as production.

I verify the following separately:

  • Tap URLs in messages with a TestFlight build installed
  • Open the same URL with the app deleted
  • Confirm production and dev domains are not confused
  • Confirm the AASA URL returns JSON without redirects
  • Test with users already in the organization and non-members

With Expo Router, paths and file structure correspond, so creating the invite screen inside the app was simple. The time-consuming part was not the app, but associating the domain with iOS.

References

Comments (0)

Sign in to join the discussion

Be the first to comment!