Cookie handoff
How the link handler passes a link to a website without putting it in the address.
Why
If the handler sent people to https://web.aquagen.co.in/?link=5W71OC4W, that address would stay in the browser's history, address-bar suggestions and bookmarks, even though the app removes ?link= from the visible address as soon as it loads. Picking that suggestion later, even weeks later, would run the link again: another destination open and another deep_link_handled event.
Instead the handler leaves the link in a cookie and redirects to the clean site address. There is nothing in the history to reopen.
Other options considered:
| Option | Why not |
|---|---|
| A freshness timestamp in the URL | Works, but leaves the link in history |
#link=… instead of ?link=… | The browser keeps the fragment in history and bookmarks too |
| Remember handled ids | Breaks campaign links that many people legitimately open twice |
How it works
dl.aquagen.co.in/d/ABCD1234?utm=email&data=...
│ handler sets cookie dl_link = { link, data, utm } (Domain=.aquagen.co.in, 2 minutes)
│ and reads it back to confirm the browser accepted it
▼
https://web.aquagen.co.in/ ← clean address, nothing to reopen
│ DeepLinkService.capture(): reads dl_link, deletes it, saves the link as pending
▼
normal flow: sign-in, resolve, merge, route
The handler (dl.aquagen.co.in) and the sites it opens (web, lakepulse, rwi, aquarecycle on .aquagen.co.in) share a parent domain, so a cookie set with Domain=.aquagen.co.in is visible to all of them.
The cookie
| Attribute | Value |
|---|---|
| Name | dl_link |
| Value | URL-encoded JSON: { "link": "ABCD1234", "data": "<base64>", "utm": "email,weekly" } (data and utm only when present) |
| Domain | .aquagen.co.in (setting VITE_COOKIE_DOMAIN) |
| Path | / |
| Max-Age | 120 seconds |
| Flags | Secure, SameSite=Lax |
It is single use: the app deletes it as soon as it reads it, whether or not the URL also had a link.
When cookies are not available
The handler falls back to the old address, https://web.aquagen.co.in/?link=ABCD1234&utm=…&data=…, when:
- the browser blocks the cookie (the handler writes it, reads it back, and sees it is missing);
VITE_COOKIE_DOMAINis empty (local development, where a cookie for.aquagen.co.incannot be set);- the value would be larger than about 3.5 KB (browsers drop cookies over ~4 KB).
In the fallback the app still removes ?link= from the address after reading it, but the original address stays in history. If both a cookie and ?link= are present, the URL wins and the cookie is still deleted.
What it does not cover
- Phones with the app. The app opens through
aquagen://orintent://, not a website, so no cookie is set. The cookie is only written when the handler is sending the person to a website. - Store redirects. A phone without the app goes to the Play Store or App Store, with no cookie.
Notes
- Cookie reach. For up to 2 minutes the browser also sends the cookie to every
*.aquagen.co.inhost it visits, including the API, so the link id anddataappear in those requests. Keep sensitive values out ofdata; the dynamic params are meant for non-secret details. - Anything on a sibling subdomain can set
dl_link. The app treats the cookie like the URL: the link id must match the expected format, anddataonly fills the names the link allows. - Security of the flow itself. The cookie carries no authority. It names a link; the link's destination still comes from the server.
Code
| File | Part |
|---|---|
deeplink_handler_web/src/handoff.js | setHandoff(): builds, sets and verifies the cookie |
deeplink_handler_web/src/App.jsx | Calls it for desktop and website-only prefixes; falls back to ?link= |
libs/shared/src/services/deeplink/deeplink.js | takeHandoffCookie() and processDeeplink() |
Testing it
- On the deployed handler, open a link on desktop. The address after the redirect should be just
https://web.aquagen.co.in/, and the link should still open its destination after sign-in. - Check the browser's history: it should show only the clean address.
- Block cookies for the site and open the link again. The address should now contain
?link=…, and the link should still work.