Skip to main content

Deep Links

A deep link is a short link (WhatsApp, email, QR code) that opens a specific screen, path or web page, with optional tracking. It is created in the admin app, served by a small link handler, resolved by an API, and acted on by this app.

For the admin-side how-to (creating links, recipes, URL building) see the Admin guide. This page is for developers.


The pieces​

PieceRepo / serviceJob
Admin appadmin-app-frontendCreate, edit, switch off, delete and customise links. Holds the list of features per module.
Admin APIadmin-app-backend (/api/admin/deepLink)Stores links in the Cosmos container deep_link. Paged, filtered list. Soft delete. Needs an admin login.
Public APIaquagenapi (/api/user/deepLink)resolve/<id>: what a link opens (no login). pixel/<id>: the email open pixel.
Link handlerdeeplink_handler_web → dl.aquagen.co.inStatic page. Reads the prefix and id, records the click, sends the person to the app or site.
This appaquagen_web_apppCaptures the link, resolves it, merges the URL's extras, routes, records deep_link_handled.

URL format​

https://dl.aquagen.co.in/<prefix>/<LINK_ID>?utm=a,b&data=<base64url JSON>
PrefixOpens
dAquaGen: the mobile app if installed (otherwise the stores), the website on desktop
arAquaRecycle website (the uwms app)
lpLakePulse website
rwiRain Water Intelligence website
iNot a site: the email open pixel, served by aquagenapi at /api/user/deepLink/pixel/<id>

The link id is generated (8 characters, A–Z and 0–9). Ids starting with TEST_ are test links: they work normally but record no analytics events.


End-to-end flow​

Person taps  https://dl.aquagen.co.in/d/ABCD1234?utm=email&data=...
│
▼
Link handler ── sends deep_link_click (user "deeplink_handler"; skipped for TEST_ links)
│
├─ phone, app installed ─────────► app opens via aquagen://d/ABCD1234?... (intent:// on Android)
├─ phone, no app (prefix d) ─────► Play Store / App Store
└─ desktop, or ar / lp / rwi ────► website, link handed over in a cookie (see Cookie handoff)
│
▼
This app (DeepLinkService.capture, before the router and splash)
│ saves { link, data, utm } as pending
▼
After sign-in (MainLayout → DeepLinkService.redirect)
│ GET /api/user/deepLink/resolve/<id> → { id, title, mobileCompatible, target }
│ 404 (off / deleted / unknown) → toast "This link is no longer active"
│ merge target + data + utm
│ record deep_link_handled (skipped for TEST_ links)
▼
HandleRedirect.handle → navigate to the screen, or open the URL

Nothing in the URL says where the link goes. That is stored on the server, so a link can be changed or switched off after it was shared.


Stored in the Cosmos container deep_link (partition key /id).

FieldMeaning
idGenerated id, also the URL's last part. TEST_ prefix for test links
typeAlways "link"
title, descriptionLabels for admins; the title is also sent to Mixpanel
moduled, ar, lp, rwi or i. Decides the prefix. Fixed once shared
activeOff links resolve as not found
mobileCompatibleWhether the mobile app may follow the link (default false)
targetWhat it opens (below)
deleted, deletedAt, deletedBySoft delete. Deleted links are filtered out of every query and resolve as not found
createdAt, createdBy, updatedAt, updatedByAudit

target is one of:

{ "mode": "feature", "feature": "ALERTS", "params": { "range": "week" }, "dynamicParams": ["email"], "utm": ["sms"] }
{ "mode": "path", "path": "/reports", "params": { } }
{ "mode": "custom", "type": "URL", "params": { "url": "https://example.com", "location": "inapp" } }
{ "mode": "custom", "type": "EMAIL_PIXEL", "params": { "imageUrl": "https://…" }, "dynamicParams": ["email"] }

All in libs/shared/src/services/deeplink/, plus one hook in the layout.

FileRole
deeplink.jsDeepLinkService. capture() runs from AppStore before the router, reads the cookie or ?link=, stores the pending link. redirect() runs from MainLayout after sign-in: resolve, merge, event, navigate. On Capacitor it also listens for appUrlOpen and the launch URL.
redirectHandler.jsHandleRedirect.handle(target, navHelper, loginData, navigate). Does the navigating itself and returns nothing.
libs/components/src/pages/layouts/MainLayout.jsxCalls attach() / redirect(), shows the "no longer active" toast.
libs/shared/src/services/api/urls.jsdeepLinkBaseUrl for the resolve call.
libs/shared/src/enums/analyticsEnum.jsDEEP_LINK_HANDLED: 'deep_link_handled'.
mixPanelAnalytics.jsAdds deepLinkId and deepLinkTitle to later events in the session.

Merge rules​

  • The saved link decides where it goes; data can never change mode, feature, path, type or the URL.
  • data may only set the names in the target's dynamicParams, and only simple values (string, number, boolean). It overrides a saved param of the same name.
  • utm in the URL replaces the link's default utm; with none in the URL the defaults apply.
  • The result's params are added to the route as a query string.

Routing by mode​

ModeWhat HandleRedirect does
featureLooks the key up in the active app's navigation helper (routes, sideBarDetails for the permission check). Unknown key, or no permission → the app's home or locked page. Special keys live in featureHandlers (see below).
pathMust start with / and must not start with // or /\; otherwise home.
custom + URLOpens params.url (http/https only): in-app browser (@capacitor/browser on a device, same tab on web) or external.
any, on the mobile app, with mobileCompatible offOpens the home page and does nothing else.

Adding a feature that needs its own routing​

  1. If the screen is in the active app's navigation helper, it works already by its key.
  2. If not (the energy pages are an example), add an entry to featureHandlers in redirectHandler.js that returns the path. ENERGY_MONITORING returns /monitoring/energy_category/ENERGY_CATEGORY/CONSUMPTION, the same address the PowerGen toggle uses.
  3. Add the key to FEATURES_BY_MODULE in the admin app (src/deeplink/schemas.js), and mark mobile: true if the mobile app has the screen.

Each module has its own routes, so the admin app's feature list depends on the module chosen: AquaGen, AquaRecycle, LakePulse and Rain Water Intelligence each list only their own screens.


Analytics​

EventFromNotes
deep_link_clickLink handlerProperties id, utm, dynamicParams. Mixpanel user is always deeplink_handler
deep_link_handledThis appid, title, utm, dynamicParams (allowed names only)
deep_link_openaquagenapi pixelid, title, utm, allowed params. User is the email param, else email_pixel

Test links send none of them. See Analytics Events for the full event list.


Where things are configured​

WhatWhere
Web origins, store links, Mixpanel token, cookie domaindeeplink_handler_web/.env.production (VITE_WEB_ORIGIN, VITE_AR_ORIGIN, VITE_LP_ORIGIN, VITE_RWI_ORIGIN, VITE_PLAY_STORE_URL, VITE_APP_STORE_URL, VITE_MIXPANEL_TOKEN, VITE_COOKIE_DOMAIN)
Link domain and pixel address used in the adminadmin-app-backend/app/services/deep_link.py (LINK_ORIGIN, PIXEL_ORIGIN)
Pixel limits, TEST_ prefix, Mixpanel URLaquagenapi/app/constants/constants.py; token in Config.MIXPANEL_TOKEN (env MIXPANEL_TOKEN)
Resolve API address in this applibs/shared/src/services/api/urls.js

Deploying a change​

ChangedRedeploy
Public API (resolve, pixel)aquagenapi
Link fields, list, deleteadmin-app-backend, then the admin app
Handler (prefixes, cookie, store links, click event)deeplink_handler_web (npm run deploy, hosting only)
libs/shared deep-link code or MainLayoutAll four web apps (production, lakepulse, rwi, uwms) and the mobile app (a new build and store release)

Deploy the APIs first, then the handler, then the apps, then the admin app.


See also​