Cross-domain tracking counts one person who moves between your hostnames, such as www.example.com, app.example.com and a checkout on another domain, as one visitor in one visit instead of several. Cookie-based tools do it by sharing a cookie across subdomains and copying the visitor’s id into the URL for other domains (GA4’s _gl, Matomo’s pk_vid); cookieless tools do it only when the part of the hostname that changes is left out of their visitor hash.
Below are the four cases that need it, what GA4, Plausible, Fathom, Umami, Matomo, PostHog and invisit, the cookieless analytics tool we build, do in each, from each vendor’s docs and source code as of November 2026, and how to check your setup. We tested GA4’s linker and invisit’s behaviour in Chrome, and each section says how. Why analytics numbers differ has the short version of this for comparing two tools.
The four cross-domain cases, and what splits a visitor in each
Each case breaks for a different reason, so name yours before you change any setting:
| Case | Example | What splits the visitor |
|---|---|---|
| www and the bare domain | www.example.com and example.com |
A cookie set on one host only, or a tool that compares exact hostnames |
| Subdomains | example.com, app., docs., blog. |
A cookie scoped to one subdomain, or a hash that includes the hostname |
| Separate domains you own | example.com and example-shop.com |
First-party cookies cannot be read on another domain |
| Pages you do not control | checkout.stripe.com, a booking widget’s domain |
No tag runs there, and the return arrives with their domain as referrer |
The first three are your own pages, so the fix is configuration. The fourth cannot be joined, only kept from stealing credit: you want the visitor’s return from the payment page to stay in the same visit, with its original source.
How does each analytics tool handle subdomains and cross-domain tracking?
As of November 2026, from each vendor’s documentation, plus Plausible’s and Umami’s source code where the docs say nothing:
| Tool | www and bare domain | Subdomains | Separate domains you own | Third-party pages |
|---|---|---|---|---|
| GA4 | One user: cookies go on the highest domain | One user with no setup, unless you set cookie_domain |
Configure your domains; ids travel in _gl for two minutes |
List unwanted referrals, up to 50 per stream |
| Plausible | One visitor: the hash uses the registrable domain | One session when every subdomain reports to one site | One dashboard possible, but a visitor counts once per domain | A return within 30 minutes keeps the session |
| Fathom | One visitor; one page row while Multi-domains off | Multi-domains, Combined: one visitor | Same setting; Separate counts each domain apart | First page view’s referrer kept for the session |
| Umami | One session; www. ignored in referrers |
One session; the other subdomain saved as referrer | One session with one website id; not documented | Saved as the page view’s referrer; no exclusion list |
| Matomo | setCookieDomain('*.example.com') |
setCookieDomain and setDomains |
Alias URLs and enableCrossDomainLinking; pk_vid, 180 s |
Excluded referrers, or ignore_referrer=1 |
| PostHog | Shared cookie, cross_subdomain_cookie |
Same; off on vercel.app, netlify.app, herokuapp.com |
Pass distinct_id and session_id, then bootstrap |
The session keeps its first referrer |
| invisit | One visit; www. ignored in referrers and pages |
One visit; hosts above and below are not referrers | One visit when both report to one site | Visit keeps its source; the provider shows in Sources |
Sources: GA4 (cookies, cross-domain, linker, unwanted referrals), Plausible (subdomains, event.ex, cache_store.ex), Fathom (multi-domains, measurement), Umami (route.ts), Matomo (JavaScript guide, cross domain linking, referral exclusion), PostHog (persistence, config, cross-domain tutorial).
How to set up cross-domain tracking in GA4
GA4 follows a visitor across www and subdomains with no setup, and across separate domains once you list them under Configure your domains. By default the Google tag sets its cookies on the highest domain it can, so blog.example.com writes them to example.com and every subdomain reads the same _ga id. A cookie_domain set to one subdomain undoes that, and Google names subdomains with different cookie domains as a cause of self-referrals.
For separate domains, the cookie cannot be read across, so GA4 copies the ids into the URL:
- In Admin, under Data collection and modification, open Data streams and your web stream.
- Open Configure tag settings, then Configure your domains, and add each domain. It takes up to 100 conditions and the Editor role.
- Put the same
G-tag from the same stream on every domain.
When a visitor clicks a link or submits a form to a listed domain, the tag appends a _gl parameter, and the tag on the destination stores the ids it carries. Google’s developer guide says the parameter expires after two minutes, which is why it is added at the moment of the click.
Google lists what breaks it: a redirect that drops _gl, a navigation started by JavaScript instead of a user’s click, and a script that stops the click event before it reaches the document. If you set the linker in code, forms are decorated only with decorate_forms, which is off by default:
<script>
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
// Before config: decorate links and forms that point to the other domain.
gtag('set', 'linker', { domains: ['example-shop.com'], decorate_forms: true })
gtag('js', new Date())
gtag('config', 'G-XXXXXXXXXX')
</script>We ran this with the live gtag.js in Chrome 155, on two hostnames mapped to 127.0.0.1, with a fresh browser for each case. A link and a GET form arrived with _gl, and the second domain stored the first domain’s _ga client id. A button that set location.href, a link through a redirect that dropped the query string, and the same form without decorate_forms all arrived without it and got a new client id: two users. Google’s Tag Manager instructions warn that a linker parameter overrides the domains set in Admin, so a code linker should list every domain itself.
How Plausible tracks subdomains and multiple domains
Plausible follows a visitor across subdomains when you add the root domain as one site and put the same snippet on every subdomain; the original source stays with the visitor as they move (Plausible). Its source code shows why: the visitor id hashes the user agent, the IP address, the site’s domain and the registrable domain of the page’s hostname, so www.example.com, app.example.com and example.com hash alike. A session continues while events arrive within 30 minutes, and later page views never replace its referrer.
Separate domains are where Plausible stops. You can send example.de and example.nl to one site and filter by hostname, but Plausible says a visitor of both counts as a unique visitor on each, and the registrable domain in the hash is the reason. The alternative is a site per domain plus the Consolidated View, a Business plan feature that sums every site in a team.
Plausible’s hostname filter and Shields, Hostnames list (up to 30, where *example.com covers the root and its subdomains) let you split or restrict one site by host.
Fathom’s multi-domain setting: Combined or Separate
Fathom counts every hostname that uses one site ID in that site, and its Multi-domains setting decides how they count (multi-domains). With Combined, a visitor going from cat.com to shop.cat.com to docs.cat.com is one visitor with three views, counted on the domain loaded first; with Separate, three visitors. Fathom’s signature includes the hostname only when the setting asks for separate visitors (data journey).
Turning Multi-domains on adds a Domains tab and groups pages by domain and path; with it off, www and bare variants of a path share one row (duplicate pathnames). Referrers are first touch per session: the first page view’s referrer is carried across every later page view and event (measurement).
How Umami handles subdomains and other domains
Umami’s docs say nothing about subdomains or other domains, so we read its source at the October 23, 2026 commit. The session id is a hash of the website id, IP address, user agent and a salt, with no hostname, so any page that sends the same website id continues the session.
Referrers are weaker. The server drops a referrer only when it equals the page’s own hostname after removing www. (route.ts), so a click from app.example.com to example.com saves app.example.com as that page view’s referrer, and the referrer report counts it. data-domains on the script tag limits where it runs, by exact hostname (tracker configuration).
Matomo: setCookieDomain, setDomains and cross-domain linking
Matomo needs code for both cases. Its visitor id cookie belongs to the page’s own domain by default, so for www, the bare domain and subdomains, add two lines before trackPageView on every page (JavaScript guide):
// Share the visitor cookie across example.com and every subdomain, and stop clicks
// between them from counting as outlinks.
_paq.push(['setCookieDomain', '*.example.com'])
_paq.push(['setDomains', '*.example.com'])For separate domains, list them as Alias URLs on the website, which also keeps them out of the Referrers report, then tick Enable Cross Domain linking in the tracking code generator and paste the result on all of them (cross domain linking). On a click, the tracker appends pk_vid with the visitor id, and the two pages are joined if the next page loads within 180 seconds, changeable with setCrossDomainLinkingTimeout (JavaScript reference).
Matomo’s FAQ adds three limits: cross-domain linking does not work with cookies disabled, links added by JavaScript after the page loads need getCrossDomainLinkingUrlParameter, and it works within one website, never across a Roll-Up.
PostHog: cross-subdomain cookies and bootstrapped ids
PostHog follows visitors across your subdomains by default and across separate domains only if you pass its ids along. posthog-js keeps the distinct id and session id in a first-party cookie, ph_<project_token>_posthog, for 365 days (persistence), and cross_subdomain_cookie sets it on the top-level domain so every subdomain reads it. PostHog turns that off on subdomains of herokuapp.com, vercel.app and netlify.app (config).
For separate domains, PostHog’s tutorial reads posthog.get_distinct_id() and posthog.get_session_id() on the first site, puts them in the link’s # fragment, and passes them to bootstrap in posthog.init() on the second. The session id must be a UUID v7 (bootstrapping). posthog-js keeps a session’s referrer for the whole session, a July 2024 fix for visitors sent to checkout.stripe.com and back (PR #1314).
How invisit follows one visitor across hostnames
invisit follows a visitor across every hostname of a site without passing anything in the URL, because the hostname is not part of its visitor hash. The hash covers the site, the UTC date, that day’s salt, the device, browser and OS type, and the IP address, and visits are stitched from it with a 30-minute gap and a 24-hour cap. A visitor who goes from example.com to app.example.com, or to another domain on the same site, keeps the same visit.
Three settings decide what you see:
- Allowed domains, under Settings, Collection: events are accepted from each listed domain and its subdomains, up to 10 per site, plain hostnames only. With the list empty, any origin is accepted; once it has entries, a second registrable domain such as
example-shop.commust be listed itself, or its events are refused. Our internal traffic guide covers the same list for previews. - The referrer rule: a page view’s referrer counts as your own, and is dropped, when it is the page’s own host, a host above or below it (
example.comandapp.example.com), or a domain on that list or one of its subdomains, after removingwww.. Sibling subdomains such asapp.anddocs.are only treated as one site once the apexexample.comis listed, because platform subdomains such asalice.github.ioandbob.github.ioare not one site. - Hostnames in reports: the Pages report keeps host and path apart, with
www.removed, and Show hostnames automatically, under Settings, General, labels pages with their host when the range spans more than one. The Landed on filter keeps visits that started on a given hostname.
Links between your own hosts are not outbound clicks: www and the bare domain, a host above or below the page’s, and anything under an allowed domain stay internal, so they record no outbound_link event and nothing billed. Only a click to another site does; our outbound link guide compares the other tools.
Put every hostname you want to follow in one site: a visit never spans two sites, and an identify() hash includes the site, so two sites cannot be joined later. The visitor’s address must also stay the same, so send every hostname through the same collector path; a proxy on one host and a direct connection on another can see one visitor’s IPv4 address on one and IPv6 on the other. And since every move to another hostname is a full page load, a move after midnight UTC starts a new visit, because the visitor hash changes with the date.
What we tested
On November 3, 2026 we served one test page on five hostnames mapped to 127.0.0.1 and walked it in headless Chrome 155 with Playwright 1.56.1. The invisit script carried data-debug="true", since it sends nothing under automation otherwise, and reported to our development stack for a site whose only allowed domain is qa.example.com. We read the result through the REST API’s visit-log and visits/{visit_id} endpoints.
| Step | Page | Referrer recorded on that page view |
|---|---|---|
| 1 | qa.example.com/home, from news.test |
news.test, which became the visit’s source |
| 2 | app.qa.example.com/app |
None: a host below the page’s |
| 3 | www.qa.example.com/pricing |
None: a host below the page’s |
| 4 | qa.example.com/plans |
None: same host once www. is removed |
| 5 | pay.checkout.test/checkout, no script |
Not tracked |
| 6 | qa.example.com/thanks |
pay.checkout.test |
The result was one visit of five page views, with news.test as its source and qa.example.com as its landing and exit host; the Pages report filed the www. page under qa.example.com. Of the five moves between hosts, only the one to pay.checkout.test recorded an outbound_link event, and Sources listed the visit once, under news.test. Our first run that day, before invisit treated its own hosts as internal and counted Sources per visit, recorded four outbound events, app.qa.example.com as the referrer on step 3, and a Sources row each for app.qa.example.com and pay.checkout.test.
Can you do cross-domain tracking without cookies?
Yes, if the tool leaves the hostname out of its visitor hash and the visitor keeps the same IP address and browser while they move. A cookieless tool has no id to copy into a URL, so the hash inputs decide it:
| Tool | Hostname in the hash | Moving to another hostname |
|---|---|---|
| Plausible | Registrable domain only | Subdomains stay one visitor; other domains do not |
| Fathom | Only with Multi-domains set to Separate | Combined keeps one visitor across the site’s domains |
| Umami | No | Any page with the website id continues the session |
| PostHog cookieless mode | Yes | A new visitor on every hostname |
| invisit | No | Any hostname reporting to the site continues the visit |
| Simple Analytics | No identifier | A referrer from another hostname starts a new visit |
| Cloudflare | No identifier | A referrer that does not match the hostname starts a new visit |
Sources: PostHog cookieless tracking, which hashes team_id, daily salt, IP address, user agent and hostname; Simple Analytics, which lets you list non-unique hostnames such as a payment provider; Cloudflare Web Analytics.
The salt sets the outer limit: one day for Plausible, Fathom, PostHog and invisit, a month by default for Umami. In each of these hash-based tools, an address change in the middle of a journey splits it. How cookieless analytics counts visitors covers both.
Why do your own subdomains or Stripe show up as referrers?
Your own hostnames in a referrer report are self-referrals: a crossing the tool did not join, or a page view whose referrer it kept mid-visit. A payment provider such as checkout.stripe.com in the same list means visitors returning from checkout were credited to it.
How each tool removes them:
- GA4 ignores a referrer from the current page’s domain or its subdomains, and one carried in by
_gl. Add payment providers under Configure tag settings, List unwanted referrals, up to 50 per stream, and the tag sendsignore_referrer=trueon matching events (unwanted referrals). A visitor credited to the provider before you listed it can keep that credit on later direct visits, under last non-direct click. - Matomo keeps Alias URLs out of Referrers. Since Matomo 4.12.0, add payment providers to Excluded referrers on the website, or add
ignore_referrer=1to the URL the provider sends visitors back to (referral exclusion). - PostHog calls a self-referral share under 5% normal and names two causes: domains without bootstrapped ids, and sessions that expired after 30 minutes idle, adjustable with
session_idle_timeout_seconds(troubleshooting). - Plausible and Fathom credit the first referrer of the session, so a return within the session does not change its source.
- invisit keeps the visit’s source, and its Sources report counts each visit once under that source, so a return from the provider changes neither Sources nor the Source filter. The provider still shows as the referrer of that one page view in the visit log, and becomes a source only when the visit timed out at checkout and a new one started. Your own hosts are recognised without a list, siblings once the apex is listed; there is no exclusion list for third-party domains.
How to check your cross-domain setup
Walk the journey yourself once, then look for its traces in the reports:
-
List every hostname a visitor can pass through:
wwwand the bare domain, each subdomain, other domains you own, and the payment or booking pages you send people to. -
In a private window, open your landing page with a tag you can find, such as
?utm_source=xdomain-test, and click through each hostname and back from the payment page. -
Watch the address bar on arrival. GA4 should add
_gl=, Matomopk_vid=; if it is missing, look for a redirect that drops the query string. -
Find the test visit and check that it is one visit with every page and your tag as its source. In invisit, the REST API returns it with each page’s hostname and referrer, using a key with the
readscope:Terminal # Visits that started in the last hour, newest first curl "https://api.invisit.dev/v1/sites/<site id>/analytics/visit-log?from=2026-11-03T18:00:00Z&to=2026-11-03T19:00:00Z&timezone=UTC" \ -H "Authorization: Bearer invisit_sk_..." # One visit, step by step curl "https://api.invisit.dev/v1/sites/<site id>/analytics/visits/<visit id>?timezone=UTC" \ -H "Authorization: Bearer invisit_sk_..." -
Open the referrer report for the last 30 days and search it for your own hostnames and payment providers. Each row is a crossing to fix with the settings above.
Checklist for tracking one visitor across domains
- Every hostname in the journey loads the same tag or site id.
-
wwwredirects to the bare domain, or the reverse, keeping the query string. - GA4: separate domains are listed under Configure your domains, and a test link and form both arrive with
_gl. - Matomo:
setCookieDomainandsetDomainson every page; Alias URLs and cross-domain linking for other domains. - PostHog: ids bootstrapped on any domain that does not share your parent domain.
- invisit: every hostname you link from is in Allowed domains, all in one site.
- Payment providers are in GA4’s unwanted referrals or Matomo’s Excluded referrers.
- A test walk-through shows one visit, and your own hostnames are gone from the referrer report.
