von der App-Site-Association bis zur getesteten Navigation
Custom-URL-Schemes lassen sich von jeder beliebigen App beanspruchen und öffnen damit ein Einfallstor für Hijacking. Universal Links unter iOS und App Links unter Android lösen das über eine kryptografisch verifizierte Domain, verlangen dafür aber eine korrekte Konfiguration auf Server- und App-Seite. Dieser Artikel zeigt Schritt für Schritt, wie React Native Deep Linking sauber eingerichtet, mit React Navigation verbunden und zuverlässig getestet wird.
Inhaltsverzeichnis
- 1. Was Deep Linking und Universal Links wirklich lösen
- 2. Die React Native Linking API
- 3. iOS Universal Links einrichten
- 4. Android App Links einrichten
- 5. Deep Linking mit React Navigation
- 6. Testen von Deep Links
- 7. Expo-spezifische Konfiguration
- 8. Fallback-Strategien und Edge Cases
- 9. Deep-Linking-Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Deep Linking und Universal Links wirklich lösen
Der klassische Ansatz für React Native Deep Linking ist ein Custom-URL-Scheme wie myapp://profil/42. Das Problem dabei: Ein Custom Scheme ist nicht exklusiv reserviert. Jede beliebige App auf dem Gerät kann sich für dasselbe Scheme registrieren, und welche App den Link tatsächlich öffnet, hängt von Installationsreihenfolge und Betriebssystemversion ab. Für sensible Flows wie Login-Bestätigungen oder Zahlungs-Callbacks ist das ein echtes Sicherheitsrisiko, weil eine bösartige App den Link abfangen könnte.
Universal Links unter iOS und App Links unter Android lösen dieses Problem, indem sie den Link an eine echte, per HTTPS verifizierte Domain binden. Das Betriebssystem prüft beim Öffnen des Links kryptografisch, ob die App tatsächlich vom Domain-Betreiber autorisiert wurde, bevor sie den Link statt des Browsers öffnen darf. Diese Verifizierung findet Server-seitig über eine öffentlich erreichbare Konfigurationsdatei statt, die in den folgenden Abschnitten aufgebaut wird.
Der praktische Vorteil reicht über Sicherheit hinaus: Ein Universal Link ist gleichzeitig eine normale, klickbare HTTPS-URL. Sie funktioniert in E-Mails, Messenger-Apps und Suchergebnissen genauso wie im Browser, während ein Custom Scheme in vielen Kontexten (etwa in einer E-Mail-Vorschau) gar nicht erst als Link erkannt wird. Für jedes Deep-Linking-Konzept, das über App-interne Debug-Links hinausgeht, ist die Domain-basierte Variante deshalb der richtige Ausgangspunkt.
2. Die React Native Linking API
Unabhängig davon, ob am Ende Universal Links, App Links oder ein Custom Scheme ankommen, verarbeitet React Native sie über dieselbe Linking-API. Zwei Fälle müssen dabei unterschieden werden: Wird die App durch den Link neu gestartet (Cold Start), liefert Linking.getInitialURL() die URL, mit der die App geöffnet wurde. Läuft die App bereits im Hintergrund und wird durch einen Link in den Vordergrund geholt (Warm Start), feuert stattdessen das 'url'-Event, auf das mit Linking.addEventListener('url', handler) reagiert wird.
Ein häufiger Fehler bei React Native Deep Linking ist, nur einen der beiden Fälle zu behandeln. Wer ausschließlich auf das Event hört, verpasst jeden Link, mit dem die App frisch gestartet wurde, weil das Event dann bereits verpasst ist, bevor der Listener registriert werden konnte. Die robuste Lösung prüft beim App-Start explizit getInitialURL() und registriert parallel den Event-Listener für alle weiteren Links während der Laufzeit.
Verwendet man React Navigation, muss man diese beiden Fälle in der Praxis selten manuell verdrahten, da die linking-Prop des NavigationContainer genau dieses Verhalten intern kapselt. Trotzdem lohnt sich das Verständnis der zugrunde liegenden API, weil eigene Analytics-Events oder Sonderfälle wie Deferred Deep Linking oft direkt auf Linking statt auf die Navigationsschicht zugreifen.
// LinkingHandler.js — cold start + warm start deep link handling
import { useEffect } from 'react';
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
export function useDeepLinkHandler() {
const navigation = useNavigation();
useEffect(() => {
// Cold start: app was launched directly via a link
Linking.getInitialURL().then((url) => {
if (url) handleDeepLink(url, navigation);
});
// Warm start: app was already running in the background
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url, navigation);
});
return () => subscription.remove();
}, [navigation]);
}
function handleDeepLink(url, navigation) {
const route = parseDeepLinkUrl(url);
if (route) navigation.navigate(route.name, route.params);
}
// Matches config.screens mapping used by NavigationContainer's linking prop
const linkingConfig = {
prefixes: ['https://mironsoft.de', 'react-native-demo://'],
config: {
screens: {
Profile: 'profil/:userId',
OrderDetails: 'bestellung/:orderId',
Home: '',
},
},
};
export { linkingConfig };
3. iOS Universal Links einrichten
Unter iOS beginnt die Einrichtung mit der Capability Associated Domains im Xcode-Projekt, in der die Domain als applinks:mironsoft.de eingetragen wird. Diese Capability erlaubt es der App überhaupt erst, sich für Universal Links dieser Domain registrieren zu lassen. Ohne diesen Eintrag ignoriert iOS jede Verifizierungsdatei auf dem Server vollständig, ein häufiger Grund, warum Universal Links trotz korrekter Serverkonfiguration nicht funktionieren.
Auf Serverseite muss unter /.well-known/apple-app-site-association, ohne Dateiendung, eine JSON-Datei erreichbar sein, die Team-ID und Bundle-ID der App sowie die erlaubten Pfad-Muster enthält. Entscheidend: Die Datei muss über HTTPS ohne jede Weiterleitung ausgeliefert werden, mit dem Content-Type application/json, da iOS beim ersten App-Start (und danach periodisch) genau diese Datei abruft und cached. Eine Redirect-Kette, etwa von HTTP auf HTTPS, lässt die Verifizierung stillschweigend fehlschlagen.
Ein zweiter typischer Stolperstein: Die Datei wird oft nachträglich geändert, aber iOS cached das Ergebnis der Verifizierung aggressiv. Änderungen werden erst nach einer Neuinstallation der App oder nach einigen Tagen zuverlässig übernommen. Für die Entwicklung empfiehlt es sich deshalb, die Verifizierung früh mit einem stabilen Domain-Setup zu testen, statt die Datei während der Entwicklung mehrfach täglich zu ändern.
{
"applinks": {
"details": [
{
"appIDs": ["<TEAM_ID>.de.mironsoft.app"],
"components": [
{ "/": "/profil/*", "comment": "Matches user profile deep links" },
{ "/": "/bestellung/*", "comment": "Matches order detail deep links" }
]
}
]
}
}
4. Android App Links einrichten
Das Android-Gegenstück zur apple-app-site-association ist die Datei assetlinks.json, ebenfalls unter /.well-known/ gehostet. Sie enthält den SHA256-Fingerabdruck des Signing-Keys, mit dem die App signiert wurde, und die Paket-ID. Android prüft diesen Fingerabdruck gegen das tatsächlich installierte APK, sodass nur die exakt passende, korrekt signierte App die Verifizierung besteht. Bei Verwendung von Google Play App Signing muss hierfür der Fingerabdruck des Play-Signing-Keys verwendet werden, nicht der des Upload-Keys.
In der AndroidManifest.xml muss zusätzlich ein intent-filter mit android:autoVerify="true" definiert werden, der die Domain und das Schema referenziert. Das autoVerify-Flag löst beim Installieren der App automatisch die Prüfung gegen assetlinks.json aus. Fehlt dieses Flag oder ist die Prüfung fehlgeschlagen, öffnet Android den Link weiterhin im Browser statt in der App, selbst wenn der intent-filter technisch korrekt konfiguriert ist.
Praktisch bewährt sich, die App-Link-Verifizierung nach jedem Release manuell mit den Android-Bordmitteln zu prüfen, statt sich allein auf das automatische Verhalten zu verlassen. Ein fehlgeschlagenes React Native Deep Linking-Setup unter Android zeigt sich fast immer erst beim echten Gerätetest, weil Emulatoren die Domain-Verifizierung teils anders behandeln als physische Geräte.
# AndroidManifest.xml — intent filter for verified App Links
cat > /tmp/intent-filter-snippet.xml <<'EOF'
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="mironsoft.de" />
</intent-filter>
EOF
# Verify domain association status on a connected device
adb shell pm get-app-links de.mironsoft.app
# Force re-verification after updating assetlinks.json
adb shell pm verify-app-links --re-verify de.mironsoft.app
5. Deep Linking mit React Navigation
React Navigation bietet mit der linking-Prop des NavigationContainer eine deklarative Schicht über der rohen Linking-API. Über config.screens wird jeder Bildschirmname auf ein URL-Pfadmuster gemappt, etwa Profile: 'profil/:userId'. React Navigation übernimmt dann automatisch das Parsen der URL, das Extrahieren der Parameter und das Navigieren zum passenden Screen, inklusive Cold-Start- und Warm-Start-Fall.
Bei verschachtelten Navigatoren, etwa einem Stack innerhalb eines Tab-Navigators innerhalb eines weiteren Stacks, spiegelt sich die Verschachtelung in der screens-Konfiguration wider: Jede Ebene bekommt ihr eigenes screens-Objekt, und React Navigation löst den Pfad rekursiv von außen nach innen auf. Das erlaubt es, tief verschachtelte Ziele wie einen bestimmten Tab mit einem bestimmten Detail-Screen direkt über einen einzigen Link anzusteuern, ohne die Navigationshierarchie manuell nachzubauen.
Parameter aus dem Pfad, etwa die userId aus profil/:userId, landen automatisch als Prop route.params.userId im Ziel-Screen, identisch zu einer normalen programmatischen Navigation mit navigation.navigate('Profile', { userId: '42' }). Das bedeutet, dass Screens keine Sonderbehandlung für Deep-Link-Aufrufe brauchen, solange sie ihre Parameter ohnehin konsistent über route.params lesen.
6. Testen von Deep Links
Deep Links lassen sich unter iOS im Simulator ohne echten Netzwerkzugriff auf die Produktions-Domain testen, mit dem Befehl xcrun simctl openurl booted "https://mironsoft.de/profil/42". Der Simulator öffnet den Link exakt so, wie es ein echtes Gerät nach erfolgreicher Universal-Link-Verifizierung tun würde, vorausgesetzt die App wurde mit der korrekten Associated-Domains-Capability gebaut. Schlägt das fehl und der Link öffnet stattdessen Safari, liegt der Fehler fast immer in der apple-app-site-association-Konfiguration.
Unter Android testet der Befehl adb shell am start -W -a android.intent.action.VIEW -d "https://mironsoft.de/profil/42" de.mironsoft.app denselben Fall, wobei die -W-Flag zusätzlich die Zeit bis zum Start ausgibt, was bei Performance-Analysen hilfreich ist. Die Kombination aus explizitem Paketnamen und Intent-Action stellt sicher, dass der Test wirklich die App öffnet und nicht versehentlich in einer Auswahl mehrerer Apps landet.
Für die eigentliche Verifizierungsdatei helfen Online-Validatoren, die die JSON-Struktur und den Content-Type der ausgelieferten Datei prüfen, sowie das Xcode-Konsolenlog beim App-Start, das explizite Fehlermeldungen zur Universal-Link-Verifizierung ausgibt. Der häufigste Validierungsfehler ist ein falscher Content-Type, oft text/plain statt application/json, weil viele Static-Hosting-Setups Dateien ohne Endung nicht korrekt klassifizieren.
7. Expo-spezifische Konfiguration
Im Expo-Managed-Workflow wird das grundlegende Custom Scheme über das Feld scheme in app.json definiert, während Universal Links und App Links über den Block ios.associatedDomains beziehungsweise android.intentFilters konfiguriert werden. Diese Angaben landen beim nächsten eas build automatisch in den generierten nativen Projekten, ohne dass Entitlements oder die AndroidManifest.xml manuell bearbeitet werden müssen.
Der praktische Unterschied zum Bare Workflow: Änderungen an associatedDomains erfordern einen neuen nativen Build, da Expo Go als generische Test-App keine App-spezifische Domain-Verifizierung durchführen kann. Für die lokale Entwicklung mit Universal Links ist deshalb ein Development Build über expo-dev-client notwendig, während einfache Custom-Scheme-Links auch in Expo Go funktionieren.
Wer von Expo Router statt React Navigation direkt Gebrauch macht, profitiert zusätzlich davon, dass die Dateisystem-basierte Routenstruktur automatisch ein passendes Deep-Linking-Schema erzeugt: Ein Screen unter app/profil/[userId].tsx ist ohne zusätzliche Konfiguration bereits über profil/:userId erreichbar, was manuelles Pflegen einer separaten linking-Konfiguration überflüssig macht.
8. Fallback-Strategien und Edge Cases
Ist die App beim Öffnen eines Universal Links nicht installiert, fällt iOS automatisch auf das Öffnen der zugrunde liegenden Website im Browser zurück, da ein Universal Link immer auch eine gültige, eigenständige HTTPS-URL ist. Diese Website sollte deshalb den identischen Inhalt oder zumindest eine sinnvolle Weiterleitung anbieten, statt eine Fehlerseite, da ein relevanter Teil der Nutzer die App noch gar nicht installiert hat.
Für Deferred Deep Linking, also das Merken der ursprünglichen Ziel-URL über einen App-Store-Umweg hinweg, reicht die native Universal-Link-Mechanik allein nicht aus, weil der App Store selbst keine Parameter durchreicht. Hier kommen meist zusätzliche Attribution-Dienste ins Spiel, die die Ziel-URL serverseitig zwischenspeichern und nach der Installation über Gerätefingerprinting oder Clipboard-Übergabe wieder zuordnen.
Auch während eines App-Updates kann es kurzzeitig zu inkonsistentem Verhalten kommen, wenn sich die unterstützten Pfad-Muster zwischen Versionen ändern. Deshalb sollte die apple-app-site-association- und assetlinks.json-Konfiguration möglichst abwärtskompatibel bleiben, und ein Custom Scheme als letzter Fallback in der prefixes-Liste von React Navigation ergänzt werden, falls die Domain-Verifizierung aus irgendeinem Grund fehlschlägt.
9. Deep-Linking-Ansätze im Vergleich
Die Wahl zwischen Custom Scheme, Universal Links und App Links ist keine reine Geschmacksfrage, sondern hat direkte Auswirkungen auf Sicherheit und Nutzererfahrung. Die folgende Übersicht fasst die wichtigsten Unterschiede für React Native Deep Linking zusammen.
| Ansatz | Sicherheit | Verhalten ohne App | Setup-Aufwand |
|---|---|---|---|
| Custom URL Scheme | Nicht exklusiv, Hijacking möglich | Fehler, kein Fallback | Gering |
| iOS Universal Links | Domain-verifiziert | Öffnet Website | Mittel |
| Android App Links | Domain- und Key-verifiziert | Öffnet Website | Mittel |
In der Praxis empfiehlt sich eine Kombination: Universal Links und App Links als primärer, verifizierter Mechanismus, ergänzt um ein Custom Scheme als letzten Fallback für Sonderfälle wie interne Test-Builds ohne öffentlich erreichbare Domain. Diese Kombination deckt sowohl produktive React Native Deep Linking-Flows als auch Entwicklungsszenarien ab, ohne Sicherheit zu opfern.
Mironsoft
React Native Entwicklung für iOS und Android
Deep Linking, das in Produktion wirklich funktioniert?
Wir richten Universal Links und App Links für eure React-Native-App ein, verbinden sie sauber mit React Navigation und sorgen für zuverlässiges Testing auf echten Geräten, nicht nur im Simulator.
Domain-Verifizierung
apple-app-site-association und assetlinks.json korrekt hosten und validieren
Navigation-Integration
React Navigation Linking-Konfiguration inklusive verschachtelter Screens
Testing & Monitoring
Geräte-Tests, Fallback-Strategien und Deferred-Deep-Linking-Konzepte
10. Zusammenfassung
React Native Deep Linking über Universal Links und App Links löst das Sicherheitsproblem klassischer Custom Schemes, indem die Verifizierung an eine echte, per HTTPS geprüfte Domain gebunden wird. Die zentrale Datei unter iOS ist die apple-app-site-association, unter Android assetlinks.json, beide unter /.well-known/ gehostet und ohne Redirect erreichbar. Die Associated-Domains-Capability und der autoVerify-Intent-Filter sind die jeweiligen App-seitigen Gegenstücke, ohne die die Server-Konfiguration wirkungslos bleibt.
React Navigation kapselt die eigentliche Navigation über die linking-Prop, inklusive Cold-Start- und Warm-Start-Fall und verschachtelter Screens. Getestet wird zuverlässig über xcrun simctl openurl und adb shell am start, während echte Geräte-Tests vor jedem Release Pflicht bleiben, weil Domain-Verifizierung auf Emulatoren nicht immer identisch funktioniert. Ein Custom Scheme als letzter Fallback rundet ein robustes Deep-Linking-Setup ab, ohne die Sicherheitsvorteile der Domain-basierten Ansätze aufzugeben.
React Native Deep Linking und Universal Links — Das Wichtigste auf einen Blick
Domain-Verifizierung
apple-app-site-association und assetlinks.json unter /.well-known/, ohne Redirect, mit korrektem Content-Type.
App-seitige Konfiguration
Associated Domains Capability (iOS) und android:autoVerify="true" Intent-Filter (Android) sind Pflicht.
React Navigation
Die linking-Prop kapselt Cold Start, Warm Start und verschachtelte Screens deklarativ.
Testing
xcrun simctl openurl und adb shell am start für Simulator/Emulator, echte Geräte vor jedem Release testen.