How to sync consent state across subdomains
Your visitor accepts cookies on the marketing site, then opens the app on a subdomain and gets the banner again, or worse, the app treats consent as unknown and tracks anyway. The fix is one shared consent record: store it on the root domain, read it the same way everywhere, and propagate changes the moment they happen.
Why each subdomain drifts on its own
Cookies default to the host that set them, and localStorage is scoped to a single origin, so a consent choice on www.example.com is invisible to app.example.com unless you deliberately share it. The symptoms are easy to miss in development and embarrassing in production: the banner reappears on every subdomain, visitors who declined get tracked by the marketing site's tags while the app shows a clean record, and your audit log holds conflicting versions of the same visitor's decision. Any one of those is a compliance problem; together they make your consent records untrustworthy.
Store the record on the root domain
The simplest reliable approach is a single consent cookie set with Domain=.example.com and Path=/, so every subdomain can read it. Mark it Secure and SameSite=Lax, and do not mark it HttpOnly, since your JavaScript needs to read it on each page load. Store a small JSON payload, not just an accept or reject flag: the version of your category schema, a timestamp, and each category the visitor decided on. The version field matters more than people expect; the day you rename or split a category, it is the only thing that tells you whether an old record still means what you think it means.
document.cookie = "cb_consent=" + encodeURIComponent(JSON.stringify({
v: 2,
ts: Date.now(),
necessary: true,
analytics: false,
marketing: false
})) + "; Domain=.example.com; Path=/; Max-Age=31536000; SameSite=Lax; Secure";
Set the expiry to match your consent renewal policy, commonly twelve months, and remember that withdrawing consent should delete or update the record rather than leaving a stale accept behind.
Read it the same way on every subdomain
Sharing the cookie is only half the job. Each property still has to interpret it the same way, which means one shared reader, not five hand-rolled parsers. Give every codebase, the marketing site, the docs subdomain, the app, the same small consent helper with one job: return the current state, or return unknown when the record is missing or its version is unrecognized. Gate every non-essential script on that helper, and treat unknown the same as declined everywhere. The most common failure mode here is not the cookie; it is the app deciding that an unparseable record means consent granted, while the marketing site treats it as declined. One helper, one rule for the unknown case, applied everywhere.
Propagate changes while tabs are open
Visitors keep multiple tabs open, so consent can change on one subdomain while another is sitting in the background. A visitor revokes marketing consent in the app tab, then switches to the docs tab, which is still holding the old state in memory and happily firing analytics. BroadcastChannel does not help here because it is same-origin only, and cross-subdomain tabs are different origins. The practical pattern is version stamping plus re-reading: stamp every record with a monotonically increasing version or a timestamp, and re-read the cookie on page visibility change and on focus. It costs almost nothing, and it closes the window where a background tab acts on a decision the visitor already reversed.
document.addEventListener("visibilitychange", () => {
if (!document.hidden) refreshConsentState();
});
If a subdomain must react instantly rather than on the next focus, have it observe the cookie with a short-interval check, but keep the interval modest; consent changes are rare events, and polling every few seconds for something that happens once a year is a battery and performance tax nobody needs.
Handle the marketing site and app split
The marketing site and the app are usually different stacks, built by different teams, on different deploy cycles. That is fine as long as both consume the same record. Ship the consent helper as a tiny standalone snippet that both teams drop in, rather than reimplementing the logic in each framework. If the app is server-rendered, forward the consent state as a request header or a first-party value the server can read before it emits any tags, so the server-rendered page never leaks tracking before the client-side gate loads. Server and client must agree on the category names and on the unknown-means-declined rule, or you will ship a bug that only appears in production on first paint.
Test across subdomains, not just on one
The test that catches most of these problems takes five minutes: open a private window, accept everything on the marketing site, then open the app subdomain and confirm the banner stays hidden and the state matches exactly, categories and version. Then reverse it: withdraw consent on the app subdomain, switch to the marketing tab, and confirm it re-read the new state. Finally, watch the network tab while you accept and withdraw, and confirm that no tracking request fires in the gap between the decision and the propagation. Run this against every subdomain you own, not just the two you think about. The one you forgot, usually the docs site or the status page, is where the bug will live.