Deep links voor native mobiele apps: routes, tokens, "open in app"
Leer deep links voor native mobiele apps: plan routes, behandel "open in app" en draag tokens veilig over voor Kotlin en SwiftUI zonder rommelige eigen routingcode.

Wat een deep link zou moeten doen, in gewone taal
Als iemand op zijn telefoon op een link tikt, verwacht die persoon één resultaat: hij komt direct op de juiste plek. Niet ergens in de buurt. Niet op een startscherm met een zoekbalk. Niet op een inlogscherm dat vergeet waarom ze kwamen.
Een goede deep link-ervaring ziet er zo uit:
- Als de app geïnstalleerd is, opent deze precies het scherm dat de link aangeeft.
- Als de app niet geïnstalleerd is, helpt de tik nog steeds (bijvoorbeeld door een webfallback of app store-pagina te openen en de persoon na installatie terug te brengen naar dezelfde bestemming).
- Als de persoon moet inloggen, logt hij één keer in en komt hij op het bedoelde scherm, niet op de standaardstart van de app.
- Als de link een actie meegeeft (uitnodiging accepteren, bestelling bekijken, e-mail bevestigen), is die actie duidelijk en veilig.
De meeste frustratie komt van links die "soort van werken" maar de flow breken. Mensen zien het verkeerde scherm, verliezen wat ze deden, of raken in een lus: tik link, log in, land op het dashboard, tik opnieuw, nogmaals inloggen. Zelfs één extra stap kan gebruikers doen afhaken, vooral bij eenmalige acties zoals uitnodigingen of wachtwoordherstel.
Voordat je enige Kotlin- of SwiftUI-code schrijft, bepaal wat je wilt dat links betekenen. Welke schermen mogen van buitenaf geopend worden? Wat verandert als de app gesloten is versus al draait? Wat moet er gebeuren als de gebruiker uitgelogd is?
De planning voorkomt het grootste deel van de pijn later: duidelijke routes, voorspelbaar "open in app"-gedrag en een veilige manier om eencodes over te dragen zonder geheimen direct in de URL te zetten.
Deep link-typen en waar “open in app” misgaat
Niet elke "link die een app opent" gedraagt zich hetzelfde. Behandel ze als uitwisselbaar en je loopt tegen de klassieke fouten aan: de link opent op de verkeerde plek, opent een browser in plaats van de app, of werkt alleen op één platform.
Drie veelvoorkomende groepen:
- Custom schemes (bijvoorbeeld een appspecifiek scheme zoals myapp:). Makkelijk op te zetten, maar veel apps en browsers behandelen ze met voorzichtigheid.
- Universal Links (iOS) en App Links (Android). Deze gebruiken normale weblinks en kunnen de app openen wanneer die geïnstalleerd is, of terugvallen naar een website als dat niet zo is.
- In-app browser links. Links die geopend worden binnen een e-mailapp of de ingebouwde browser van een messenger. Ze gedragen zich vaak anders dan Safari of Chrome.
"Open in app" kan verschillende dingen betekenen, afhankelijk van waar de tik plaatsvindt. Een link in Safari kan direct in de app springen. Diezelfde link in een e-mail of messenger kan eerst een embedded webview openen, en de gebruiker moet dan op een extra "open"-knop drukken (of ziet die knop nooit). Op Android respecteert Chrome mogelijk App Links, terwijl de in-app browser van een sociale app ze kan negeren.
Cold start vs al draaiende app is de volgende valkuil.
- Cold start: het OS start je app, je app initialiseert en pas daarna ontvang je de deep link. Als je opstartflow een splashscreen toont, auth controleert of remote config laadt, kan de link verloren gaan tenzij je hem opslaat en later afspeelt.
- Al draaiend: je ontvangt de link terwijl de gebruiker al in de sessie zit. De navigatiestack bestaat al, dus dezelfde bestemming vereist misschien andere afhandeling (push een scherm vs reset de stack).
Een simpel voorbeeld: een uitnodigingslink uit Telegram opent vaak eerst in een in-app browser. Als je app ervan uitgaat dat het OS altijd direct overdraagt, zien gebruikers een webpagina en denken dat de link kapot is. Plan voor deze omgevingen vooraf en je schrijft later minder platform-specifieke lijm.
Plan je routes voordat je iets implementeert
De meeste deep link-bugs zijn geen Kotlin- of SwiftUI-problemen. Het zijn planningsproblemen. De link mappt niet eenduidig naar één scherm, of draagt te veel "misschien"-opties.
Begin met één consistente routepatroon die overeenkomt met hoe mensen over je app denken: lijst, detail, instellingen, checkout, uitnodiging. Houd het leesbaar en stabiel, want je hergebruikt het in e-mails, QR-codes en webpagina's.
Een eenvoudige set routes kan omvatten:
- Home
- Bestellingen (lijst) en Bestelling details (orderId)
- Accountinstellingen
- Uitnodiging accepteren (inviteId)
- Zoeken (query, tab)
Definieer dan je parameters:
- Gebruik IDs voor individuele objecten (orderId).
- Gebruik optionele parameters voor UI-state (tab, filter).
- Bepaal defaults zodat elke link één beste bestemming heeft.
Bepaal ook wat er gebeurt als de link fout is: ontbrekende data, ongeldige ID of content waar de gebruiker geen toegang toe heeft. De veiligste standaard is het dichtsbijzijnde stabiele scherm openen (zoals de lijst) en een korte melding tonen. Vermijd het dumpen van mensen op een leeg scherm of een inlogscherm zonder context.
Plan tenslotte per bron. Een QR-code heeft meestal een korte route nodig die snel opent en slecht netwerk toelaat. Een e-maillink mag langer zijn en extra context bevatten. Een webpagina-link moet gracieus degraderen: als de app niet geïnstalleerd is, moet de gebruiker nog steeds ergens landen dat uitlegt wat te doen.
Als je een backend-gedreven aanpak gebruikt (bijvoorbeeld API-endpoints en schermen genereren met een platform zoals AppMaster), wordt dit routeplan een gedeeld contract: de app weet waarheen te gaan en de backend weet welke IDs en staten geldig zijn.
Veilige token-overdracht zonder geheimen in URL's te zetten
Een deep link wordt vaak behandeld als een veilig envelopje. Dat is het niet. Alles in de URL kan in browsergeschiedenis, screenshots, gedeelde previews, analytics-logs of gekopieerd in chat terechtkomen.
Zet geen geheimen in de link. Dat omvat langlopende toegangstokens, refresh-tokens, wachtwoorden, persoonlijke gegevens of iets dat iemand anders in staat stelt als de gebruiker op te treden als de link wordt doorgestuurd.
Een veiligere patroon is een kortelevende, eenmalige code. De link draagt alleen die code en de app ruildt die in voor een echte sessie nadat hij geopend is. Als iemand de link steelt, is de code na een minuut of twee of na de eerste succesvolle inwisseling waardeloos.
Een eenvoudige overdrachtflow:
- De link bevat een eenmalige code, geen sessietoken.
- De app opent en roept je backend aan om de code in te wisselen.
- De backend valideert vervaldatum, controleert of hij niet gebruikt is en markeert hem als gebruikt.
- De backend geeft een normale geauthenticeerde sessie terug aan de app.
- De app verwijdert de code uit het geheugen zodra hij ingewisseld is.
Zelfs na een succesvolle inwisseling, bevestig identiteit in de app voordat je iets gevoeligs doet. Als de link bedoeld is om een betaling te bevestigen, een e-mail te wijzigen of data te exporteren, eis dan een snelle controle zoals biometrie of een verse login.
Sla de resulterende sessie veilig op. Op iOS betekent dat meestal Keychain. Op Android gebruik je Keystore-ondersteunde opslag. Bewaar alleen wat je nodig hebt en maak het leeg bij logout, accountverwijdering of wanneer je verdacht hergebruik detecteert.
Een concreet voorbeeld: je stuurt een uitnodigingslink naar een workspace. De link draagt een eenmalige code die na 10 minuten verloopt. De app wisselt die in en toont dan een scherm dat duidelijk aangeeft wat er vervolgens gebeurt (welk workspace). Pas nadat de gebruiker bevestigt voltooit de app de join.
Als je met AppMaster werkt, past dit netjes bij een endpoint dat codes inwisselt en een sessie terugstuurt, terwijl je mobiele UI de bevestiging afhandelt voordat een ingrijpende actie plaatsvindt.
Authenticatie en “doorgaan waar je gebleven was”
Deep links wijzen vaak naar schermen met privégegevens. Begin met bepalen wat openbaar (public) geopend mag worden en wat een ingelogde sessie vereist (protected). Deze ene beslissing voorkomt de meeste "het werkte in testen"-verrassingen.
Een eenvoudige vuistregel: link eerst naar een veilige landingsstaat en navigeer naar het beschermde scherm pas nadat je hebt bevestigd dat de gebruiker geauthenticeerd is.
Bepaal wat public vs protected is
Behandel deep links alsof ze naar de verkeerde persoon doorgestuurd kunnen worden.
- Public: marketingpagina's, helpartikelen, start van wachtwoordherstel, start van uitnodigingsacceptatie (nog geen data getoond)
- Protected: bestelgegevens, berichten, accountinstellingen, admin-schermen
- Gemengd: een preview-scherm kan prima zijn, maar toon alleen niet-gevoelige placeholders totdat login bevestigd is
“Doorgaan na login” zodat je op de juiste plek terugkomt
De betrouwbare aanpak is: parse de link, sla de bedoelde bestemming op en routeer op basis van auth-state.
Voorbeeld: een gebruiker tikt op een "open in app"-link naar een specifiek supportticket terwijl hij uitgelogd is. Je app zou moeten openen naar een neutraal scherm, vragen om in te loggen en daarna automatisch naar dat ticket navigeren.
Om het betrouwbaar te houden, sla lokaal een klein "return target" op (routenaam plus ticket-ID) met een korte vervaltijd. Na het voltooien van login lees je het één keer, navigeer je en wis je het. Als login faalt of het target is verlopen, val je terug op een veilige homepagina.
Behandel randgevallen met respect:
- Verlopen sessie: toon een korte melding, laat opnieuw authenticeren en ga dan door.
- Intrekking van toegang: open de bestemmingsshell en toon vervolgens "Je hebt geen toegang meer" en bied een veilig vervolg.
Vermijd ook het tonen van privégegevens in lockscreen-previews, app-switcher-screenshots of notificatiepreviews. Houd gevoelige schermen leeg totdat data geladen is en de sessie geverifieerd is.
Een routeringsaanpak die custom navigatie-spaghetti voorkomt
Deep links worden rommelig als elk scherm URL's op zijn eigen manier parseert. Dat verspreidt kleine beslissingen (wat is optioneel, wat is verplicht, wat is geldig) door de hele app en maakt het moeilijk om veilig te veranderen.
Behandel routing als gedeelde infrastructuur. Hou één routetabel en één parser, en laat de UI schone inputs ontvangen.
Gebruik één gedeelde routetabel
Laat iOS en Android het eens zijn over één leesbare lijst met routes. Zie het als een contract.
Elke route mappt naar:
- een scherm, en
- een klein inputmodel.
Bijvoorbeeld: "Order details" mappt naar een Order-scherm met een input zoals OrderRouteInput(id). Als een route extra waarden nodig heeft (zoals een ref source), horen die in dat inputmodel, niet verspreid door de view-code.
Centraliseer parsing en validatie
Houd parsing, decodering en validatie op één plek. De UI hoeft niet te vragen "Is deze token aanwezig?" of "Is deze ID geldig?". Ze moet óf een geldig route-input ontvangen, óf een duidelijke foutstaat.
Een praktische flow:
- Ontvang de URL (tik, scan, share sheet)
- Parseer naar een bekende route
- Valideer verplichte velden en toegestane formaten
- Produceer een schermtarget plus een inputmodel
- Navigeer via één toegangspunt
Voeg een "onbekende link" fallback-scherm toe. Maak het nuttig, niet een doodlopend eind: toon wat niet geopend kon worden, leg in gewone taal uit waarom en bied vervolgstappen zoals Home, zoeken of inloggen.
Stapsgewijs: ontwerp deep links en "open in app"-gedrag
Goede deep links voelen saai aan, op de beste manier. Mensen tikken en komen op de juiste plek, of de app nu geïnstalleerd is of niet.
Stap 1: kies de entrypoints die ertoe doen
Maak een lijst van de eerste 10 linktypes die mensen echt gebruiken: uitnodigingen, wachtwoordreset, bestelbonnen, "view ticket"-links, promotielinks. Houd het bewust klein.
Stap 2: schrijf de patronen als een contract
Voor elk entrypoint definieer je één canoniek patroon en de minimale data die nodig is om het juiste scherm te openen. Geef de voorkeur aan stabiele IDs boven namen. Bepaal wat verplicht is en wat optioneel.
Handige regels:
- Eén doel per route (uitnodiging, reset, bon).
- Verplichte parameters zijn altijd aanwezig; optionele hebben veilige defaults.
- Gebruik dezelfde patronen op iOS (SwiftUI) en Android (Kotlin).
- Als je veranderingen verwacht, reserveer dan een eenvoudige versieprefix (zoals v1).
- Definieer wat er gebeurt als parameters ontbreken (toon een foutscherm, geen blanco pagina).
Stap 3: bepaal logingedrag en het post-login target
Schrijf per linktype op of login vereist is. Als dat zo is, onthoud de bestemming en ga verder na login.
Voorbeeld: een bonlink kan een preview tonen zonder login, maar het downloaden van de factuur kan login vereisen en moet de gebruiker terugbrengen naar diezelfde bon.
Stap 4: stel token-overdrachtsregels in (houd geheimen uit URL's)
Als de link een eenmalige token nodig heeft (uitnodiging, reset, magic sign-in), definieer hoe lang die geldig is en hoe hij gebruikt kan worden.
De praktische aanpak: de URL draagt een kortelevende, single-use code en de app wisselt die in bij je backend voor een echte sessie.
Stap 5: test de drie echte toestanden
Deep links breken aan de randen. Test elk linktype in deze situaties:
- Cold start (app gesloten)
- Warm start (app in geheugen)
- Geen app geïnstalleerd (de link landt nog steeds ergens zinvol)
Als je routes, auth-checks en tokeninwisselregels op één plek houdt, voorkom je dat custom routinglogica zich verspreidt over Kotlin- en SwiftUI-schermen.
Veelgemaakte fouten die deep links breken (en hoe ze te vermijden)
Deep links falen vaak om saaie redenen: een kleine aanname, een hernoemd scherm of een "tijdelijke" token die overal opduikt.
De storingen die je in het wild ziet (en de oplossingen)
-
Toegangstokens in de URL plaatsen (en ze lekken naar logs). Querystrings worden gekopieerd, gedeeld, in browsergeschiedenis opgeslagen en gevangen door analytics en crashlogs. Oplossing: zet alleen een korte, eenmalige code in de link, wissel die in in de app en laat hem snel verlopen.
-
Aannemen dat de app geïnstalleerd is (geen fallback). Als een link naar een foutpagina gaat of niets doet, haken gebruikers af. Oplossing: bied een fallback-webpagina die uitlegt wat er gebeurt en een normale installroute biedt. Zelfs een simpele "Open de app om door te gaan"-pagina is beter dan niets.
-
Geen rekening houden met meerdere accounts op één apparaat. Het juiste scherm openen voor de verkeerde gebruiker is erger dan een kapotte link. Oplossing: controleer bij ontvangst van een link welke account actief is, vraag bevestiging of laat de gebruiker wisselen, en ga dan verder. Als de actie een specifieke workspace vereist, voeg dan een workspace-ID toe (geen geheim) en valideer die.
-
Links breken als schermen of routes veranderen. Als je route aan UI-namen vastzit, sterven oude links zodra je een tab hernoemt. Oplossing: ontwerp stabiele, intentgebaseerde routes (invite, ticket, order) en houd oudere versies werkend.
-
Geen traceerbaarheid als er iets misgaat. Zonder mogelijkheid om te reproduceren kan support alleen raden. Oplossing: voeg een niet-gevoelig request-ID toe aan de link, log het op server en in de app, en toon een foutmelding met dat ID.
Een korte realiteitscheck: stel je een uitnodigingslink voor in een groepschat. Iemand opent hem op een werktelefoon met twee accounts, de app is niet geïnstalleerd op hun tablet en de link wordt doorgestuurd naar een collega. Als de link alleen een uitnodigingscode bevat, fallbackgedrag ondersteunt, vraagt om de juiste account en een request-ID logt, kan die enkele link in al die situaties slagen zonder geheimen prijs te geven.
Voorbeeld: een uitnodigingslink die elke keer het juiste scherm opent
Uitnodigingen zijn een klassieker voor deep links: iemand stuurt een collega een link in een messenger en de ontvanger verwacht één tik naar het uitnodigingsscherm, niet een generieke homepage.
Scenario: een manager nodigt een nieuwe supportagent uit om lid te worden van de "Support Team"-workspace. De agent tikt de uitnodiging in Telegram aan.
Als de app geïnstalleerd is, zou het systeem de app moeten openen en de uitnodigingsgegevens doorgeven. Als de app niet geïnstalleerd is, moet de gebruiker op een eenvoudige webpagina landen die uitlegt waar de uitnodiging over gaat en een installatieroute aanbiedt. Na installatie en eerste start moet de app de uitnodigingsflow kunnen afmaken zodat de gebruiker niet naar de link hoeft te zoeken.
Binnen de app is de flow op Kotlin en SwiftUI hetzelfde:
- Lees de uitnodigingscode uit de inkomende link.
- Controleer of de gebruiker ingelogd is.
- Verifieer de uitnodiging bij je backend en routeer naar het juiste scherm.
Verificatie is het punt. De link mag geen geheimen bevatten zoals een langlevend sessietoken. Hij moet een korte uitnodigingscode bevatten die alleen nuttig is nadat jouw server hem heeft gevalideerd.
De gebruikerservaring moet voorspelbaar aanvoelen:
- Niet ingelogd: ze zien een inlogscherm en keren daarna terug naar de uitnodigingsacceptatie.
- Ingelogd: ze zien één bevestiging "Word lid van workspace" en landen in de juiste workspace.
Als de uitnodiging verlopen of al gebruikt is, gooi de gebruiker niet op een leeg foutscherm. Toon een duidelijke boodschap en een vervolgstap: vraag een nieuwe uitnodiging aan, wissel account of neem contact op met een admin. "Deze uitnodiging is al geaccepteerd" is beter dan "Ongeldige token."
Snelle checklist en vervolgstappen
Deep links voelen pas "klaar" als ze zich overal hetzelfde gedragen: cold start, warm start en wanneer de gebruiker al is ingelogd.
Snelle checklist
Voor je release, test elk item op echte apparaten en OS-versies:
- De link opent het juiste scherm bij cold start en warm start.
- Er staat niets gevoeligs in de URL. Als je een token moet meegeven, maak het kortlevend en bij voorkeur eenmalig.
- Onbekende, verlopen of al gebruikte links vallen terug op een duidelijk scherm met een nuttige boodschap en een veilige vervolgstap.
- Het werkt vanuit e-mailapps, browsers, QR-scanners en messenger-previews (sommigen openen links vooraf).
- Logging vertelt wat er gebeurde (ontvangen link, geparseerde route, auth vereist, succes- of faalreden).
Een eenvoudige manier om gedrag te valideren is een handvol 'must-work' links kiezen (uitnodiging, wachtwoordreset, orderdetail, supportticket, promo) en ze door dezelfde testflow halen: tik vanuit e-mail, tik vanuit chat, scan een QR-code, open na herinstallatie.
Vervolgstappen (houd het onderhoudbaar)
Als deep links zich beginnen te verspreiden over schermen, behandel routing en auth als gedeelde infrastructuur, niet als per-schermcode. Centraliseer routeparsing op één plek en maak elk doel toegankelijk via schone parameters (geen ruwe URL's). Doe hetzelfde voor auth: één poort die beslist "nu doorgaan" vs "eerst inloggen, daarna doorgaan."
Als je minder custom glue-code wilt, helpt het om backend, auth en mobiele apps samen te bouwen. AppMaster (appmaster.io) is een no-code platform dat productieklare backends en native mobiele apps genereert, wat het makkelijker kan maken om routenamen en eencode-inwisselendpoints op elkaar af te stemmen naarmate vereisten veranderen.
Als je volgende week één ding doet: schrijf je canonieke routes op en de exacte fallback-gedraging voor elk faalgeval, implementeer die regels in één routinglaag.
FAQ
Een deep link moet precies het scherm openen dat de link bedoelt, niet een generiek start- of dashboardscherm. Als de app niet geïnstalleerd is, moet de link nog steeds helpen door de gebruiker ergens zinvol te laten landen en te begeleiden naar dezelfde bestemming na installatie.
Universal Links (iOS) en App Links (Android) gebruiken normale web-URL's en kunnen de app openen als die geïnstalleerd is, met een nette fallback wanneer dat niet zo is. Custom schemes zijn makkelijker in te stellen maar kunnen door browsers en andere apps inconsistent worden behandeld, dus gebruik die liever als secundaire optie.
Veel e-mail- en messenger-apps openen links in hun eigen ingebouwde browser, die mogelijk niet op dezelfde manier aan het OS overdraagt als Safari of Chrome. Houd rekening met een extra stap door de web-fallback duidelijk te maken en gevallen te behandelen waarin de gebruiker eerst op een webpagina landt.
Bij cold start toont je app mogelijk een splashscreen, voert startup-checks uit of laadt config voordat navigatie mogelijk is. De betrouwbare oplossing is het inkomende linkdoel meteen opslaan, de initialisatie afronden en daarna de navigatie "herafspelen" zodra de app klaar is.
Zet nooit langlopende toegangstokens, refresh-tokens, wachtwoorden of persoonlijke gegevens in de URL; URL's kunnen worden gelogd, gedeeld en gecachet. Gebruik in plaats daarvan een kortelevende, eenmalige code in de link en ruil die in bij je backend nadat de app geopend is.
Parse de link, sla de bedoelde bestemming op en routeer op basis van de authenticatiestatus zodat inloggen één keer gebeurt en de gebruiker daarna op het juiste scherm terechtkomt. Houd het opgeslagen "return target" klein en tijdelijk, en verwijder het nadat het gebruikt is.
Behandel routes als een gedeeld contract en centraliseer parsing en validatie op één plek. Geef schermen schone parameters in plaats van ruwe URL's, zodat niet elk scherm zijn eigen regels moet bedenken voor optionele parameters en foutafhandeling.
Controleer eerst welke account actief is en of die overeenkomt met de workspace of tenant die de link impliceert. Vraag de gebruiker om te bevestigen of van account te wisselen voordat je privé-inhoud toont. Een korte bevestigingsstap is beter dan per ongeluk het juiste scherm onder de verkeerde gebruiker openen.
Standaard: open het dichtstbijzijnde stabiele scherm, zoals een lijstpagina, en toon een korte boodschap die uitlegt wat niet geopend kon worden. Vermijd blanco schermen, stille fouten of het dumpen van gebruikers op een loginpagina zonder context.
Test elke belangrijke linksoort in drie toestanden: app gesloten, app al actief, en app niet geïnstalleerd. Doe het vanaf echte bronnen zoals e-mail, chat-apps en QR-scanners. Als je met AppMaster werkt, kun je routenamen en eencode-inwisselendpoints tussen backend en native apps op één lijn houden, wat de hoeveelheid custom glue-code vermindert.


