Opening Links

Every screen has an address. A link to it opens that screen with its parameters — in the installed app on iOS and Android, in the browser for the web app — whether the app was closed or already open. Nothing to design: you fill in three fields once, and each export writes what its platform needs.

What a link looks like

A screen's address is the web app's route for it, with its parameters as a query: home is /, every other screen is its name as a path. The Share action sends exactly these links, so a shared event opens the event, in the app when it is installed.

links
text
https://gather.app/event?id=42      your web address: the app when installed, else the web app
gatherattendee://event?id=42        the app's own scheme: always the app (return URLs, development)
/event?id=42                        inside the app, the same address

A share whose path you typed yourself keeps opening the website; Checks notes this on that share. The app answers only to its own screens' addresses.

Setting it up

In the App panel, under Web address, the section Opening links in the app holds:

Web addressThe domain your links use, such as https://gather.app. Set already if you publish a web app.
Apple Team IDTen capital letters and digits, from your Apple Developer account (Membership). With the iOS bundle ID, it is what lets iOS hand your links to the app.
Android fingerprintsThe SHA-256 fingerprints of the keys your app is signed with: Play App Signing's (Play Console → App integrity), your upload key, a debug key for testing. Up to five; pasted in any common form.
Link schemeThe app's own scheme. Left empty, it comes from the app's name, letters and digits only: Gather Attendee becomes gatherattendee.

Checks warns when a web address is set but iOS has no Team ID or Android no fingerprint, and says what does not work until it is filled in: the link opens the website instead of the app.

How a link opens

  • App closed — the screen opens with home beneath it, so Back stays in the app. A screen a tab owns opens in that tab, with the bar.
  • App open — the screen is pushed on top of where the person is, in the tab that owns it.
  • A private screen — someone signed out goes through sign-in first. Once they sign in, the linked screen opens, with its parameters, over wherever signing in leads; Back returns there. Only a screen they were on their way to comes back, never the one they signed out from.
  • A bad parameter — a missing id, or a record that cannot be read, shows the screen's own not-found or error state. Never a crash.
  • An unknown address — opens home.

Every screen opens from a link, with two exceptions. The screens of the sign-in flow don't: a link to one opens the signed-out screen, or home in an app without sign-in. Overlays (sheets, dialogs) have no address. Select a screen and change Opens from a link in the Inspector (Automatic, Yes or No) to decide for that screen.

What each export writes

Flutter. Koolbase never writes the ios/ or android/ folders, so the entries are ready to merge, in koolbase/links/, each saying where it goes. Merge them once; a re-export never touches your files. .koolbase/export.json lists them under required_platform_config, as “opening links in the app”.

koolbase/links/
text
Runner.entitlements      Associated Domains (applinks:gather.app) → ios/Runner/Runner.entitlements
Info.plist.xml           FlutterDeepLinkingEnabled and your scheme → ios/Runner/Info.plist
AndroidManifest.xml      the verified https filter, your scheme and
                         flutter_deeplinking_enabled → the main <activity>

Expo. app.config.ts lays assets/koolbase/links.json over app.json: the scheme, the associated domain and the verified intent filter, rewritten on every export. app/+native-intent.tsx is yours, written once: it hands each link to Koolbase's redirectSystemPath. An app exported before links existed needs that one line added; the manifest says so.

Web. The web app reads the address before its router does. And, once the Team ID or a fingerprint is set, the build carries the two files iOS and Android fetch from your domain to trust the app:

public/.well-known/
text
apple-app-site-association   TEAMID.bundle.id, every path
assetlinks.json              the Android package and its fingerprints

Serving the two files

They must be at https://your-domain/.well-known/, over HTTPS, with no redirect, served as application/json (the Apple file has no extension, so set its type explicitly on your host). The web export's build copies them from public/. If your links use the web address but your web app lives elsewhere, host these two files on the web address's domain anyway.

Checking on a device

The phone checks your domain itself, so the last step needs the real domain, a signed app and a device or simulator. Open a link from outside the app:

terminal
Bash
# iOS simulator
xcrun simctl openurl booted "https://gather.app/event?id=42"
xcrun simctl openurl booted "gatherattendee://event?id=42"

# Android (device or emulator)
adb shell am start -a android.intent.action.VIEW -d "https://gather.app/event?id=42"
adb shell pm get-app-links com.gather.attendee      # "verified" next to your domain
adb shell pm verify-app-links --re-verify com.gather.attendee
  • On iOS, a universal link typed into Safari's address bar opens the website by design. Tap it in Notes or Messages instead.
  • Apple caches the association file. After changing it, reinstall the app to fetch it again.
  • On Android, a domain shown as not verified means assetlinks.json is unreachable or lacks the fingerprint of the key this build is signed with. A debug build is signed with the debug key.
  • If a scheme link of the form gatherattendee://event does not open its screen on a device, gatherattendee:///event (three slashes) does.

Links in Koolbase's own auth emails (verification, password reset) are a separate setup: see Deep Linking Emails.