Skip to main content

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:

OptionWhy not
A freshness timestamp in the URLWorks, but leaves the link in history
#link=… instead of ?link=…The browser keeps the fragment in history and bookmarks too
Remember handled idsBreaks 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.

AttributeValue
Namedl_link
ValueURL-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-Age120 seconds
FlagsSecure, 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_DOMAIN is empty (local development, where a cookie for .aquagen.co.in cannot 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:// or intent://, 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.in host it visits, including the API, so the link id and data appear in those requests. Keep sensitive values out of data; 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, and data only 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​

FilePart
deeplink_handler_web/src/handoff.jssetHandoff(): builds, sets and verifies the cookie
deeplink_handler_web/src/App.jsxCalls it for desktop and website-only prefixes; falls back to ?link=
libs/shared/src/services/deeplink/deeplink.jstakeHandoffCookie() and processDeeplink()

Testing it​

  1. 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.
  2. Check the browser's history: it should show only the clean address.
  3. Block cookies for the site and open the link again. The address should now contain ?link=…, and the link should still work.