Skip to content

Cross-subdomain tracking

Your marketing site lives on example.com, your app on app.example.com. localStorage is per-origin: if identity lived only there, the visitor who read your pricing page and the visitor who signs up in your app would get two different anonymous ids, and the pre-signup history — with the campaign that brought them — would die at the border before identify() ever got a chance to connect it.

The default persistence is 'localStorage+cookie' with cookieDomain: 'auto', so the bare snippet crosses subdomains with zero configuration:

kilden.init('YOUR_WRITE_KEY');

The identity bundle lives in a first-party cookie scoped to the registrable domain — the only storage every subdomain can read — mirrored in localStorage. Install the snippet on both sites with the same write key and the visitor keeps the same anonymous id across the crossing; identify() at signup connects the entire pre-signup browsing history to the new user.

Identity only: the anonymous id, the distinct id, the session, the opt-out state, and the first-touch record (below). Super properties, caches and queues stay in localStorage — cookies ride the header of every request to your domain, so only what must survive the crossing goes in.

cookieDomain: 'auto' probes candidate domains widest-first and keeps the first one the browser accepts. Public suffixes (io, co.uk) bounce off the cookie jar, so auto lands on the registrable domain (eTLD+1): on app.example.co.uk the cookie is scoped to .example.co.uk. Bare hosts (localhost, IPs) get a host-only cookie.

If you’d rather not rely on detection in production, pin the scope — it’s one assumption fewer:

kilden.init('YOUR_WRITE_KEY', {
cookieDomain: '.example.com',
});

If the browser rejects the requested scope (a domain that doesn’t match the host, a public suffix), the SDK doesn’t half-work with a cookie that never sticks — it falls back to localStorage-only behavior.

And to opt out of cookies entirely:

kilden.init('YOUR_WRITE_KEY', {
persistence: 'localStorage',
});

This keeps everything in per-origin localStorage — no cookie is ever written, and identity no longer crosses subdomains.

The default writes a first-party cookie. If your setup needs to be cookie-free, opt out with persistence: 'localStorage' — just don’t assume that alone settles the compliance question: regulations such as the EU’s ePrivacy rules are generally technology-neutral, and storing information on the visitor’s device can require consent regardless of the mechanism, so check your own privacy guidance for what applies to you. What does change with cookies is that consent scanners detect them — expect the Kilden cookie to show up in automated audits, and make sure your consent banner declares it.

Migration: existing visitors keep their history

Section titled “Migration: existing visitors keep their history”

The default is safe for sites with existing traffic:

  • Adoption over freshness. A pre-upgrade localStorage identity is adopted into the cookie, not replaced — existing visitors keep their id and their history instead of forking into a stranger.
  • The cookie wins divergence. When two subdomains had different pre-upgrade ids, the first page to load writes the shared cookie and the other falls in line — deterministic, no fighting. Events already sent under the other id stay attributed to it; if that visitor identifies, identity resolution joins the trails.

On by default, no configuration. The SDK freezes two facts with different lifetimes as person traits:

  • How they first arrived — written on the very first visit, even a direct one: $initial_referrer ($direct when there is none), $initial_referring_domain, $initial_landing_page.
  • Which campaign first reached them$initial_utm_source, $initial_utm_medium, $initial_utm_campaign, $initial_utm_term, $initial_utm_content, written the first time a utm_* parameter ever appears, which may be visits later.

A direct first visit does not lock the campaign fields to nothing — a deliberate difference from PostHog, which freezes both on visit one. This answers both questions instead of one.

Everything ships as $set_once, which the pipeline applies per key: late never overwrites early, and resending is harmless. Per-event $utm_* context properties are unchanged — every event still carries the campaign of its own visit (see Events & properties).

The first-touch record is part of the identity bundle, so with the default persistence the landing site and the app agree on what was already recorded — the campaign captured on example.com is not re-frozen differently on app.example.com.

Both options are documented in the SDK configuration reference.