# Cookie consent for DataFast in Next.js without breaking attribution

> DataFast sets a year-long first-party cookie, which needs opt-in consent in the EU and UK. Gate the script render, the goal queue and the checkout metadata on one flag.

The DataFast script writes `datafast_visitor_id`, a first-party cookie that
lasts a year. It is what makes returning visitors and revenue attribution work.

Under the EU ePrivacy rules and the UK's PECR, storing that on a visitor's
device is not "strictly necessary". So in those places the script cannot run
until the visitor says yes. Elsewhere, often no banner is needed. That call
belongs to whoever owns your privacy policy. This page is about making the
code honour it.

## The three places data leaves

A consent gate that only covers the script tag leaks in two other places:

1. **The script tag.** Rendering it sets the cookie.
2. **Queued goals.** Code that calls `window.datafast("signup_started")` before
   the script loads pushes onto a queue. If the script loads later, the queue is
   sent. A goal fired before consent goes out after it.
3. **Checkout metadata.** Your server copies the visitor id into Stripe or Polar
   metadata. If the cookie exists from an earlier visit, the id leaves for a
   visitor who withdrew consent.

Gate all three on one flag.

## One flag, readable on both sides

Store the answer in a first-party cookie, not localStorage. The server can read
a cookie. It cannot read localStorage.

```ts
export const CONSENT_MODE: "required" | "implied" = "required";
export const CONSENT_COOKIE = "analytics_consent";

export function hasAnalyticsConsent(): boolean {
  if (CONSENT_MODE === "implied") return true;
  if (typeof document === "undefined") return false;
  return document.cookie.split(";").some((c) => c.trim() === `${CONSENT_COOKIE}=granted`);
}

export function grantAnalyticsConsent(): void {
  document.cookie = `${CONSENT_COOKIE}=granted; Max-Age=31536000; Path=/; SameSite=Lax`;
  window.dispatchEvent(new CustomEvent("analytics-consent-change"));
}
```

## Gate the render

Read consent after mount, so server and client HTML agree, and render the tag
only when it is true:

```tsx
"use client";

export function DataFastScript() {
  const [allowed, setAllowed] = useState(false);

  useEffect(() => {
    setAllowed(hasAnalyticsConsent());
    const update = () => setAllowed(hasAnalyticsConsent());
    window.addEventListener("analytics-consent-change", update);
    return () => window.removeEventListener("analytics-consent-change", update);
  }, []);

  if (!allowed) return null;
  return <Script src="/js/script.js" data-website-id="dfid_..." strategy="afterInteractive" />;
}
```

When the banner calls `grantAnalyticsConsent()`, the state flips and
`next/script` loads the tag on the spot. No reload.

## Gate the queue

```ts
export function trackGoal(name: string, params?: Record<string, string>): void {
  if (!hasAnalyticsConsent()) return; // drop, do not queue
  window.datafast?.(name, params);
}
```

## Gate the checkout

```ts
const jar = await cookies();
if (CONSENT_MODE === "required" && jar.get(CONSENT_COOKIE)?.value !== "granted") {
  return {}; // no attribution without consent
}
```

## Withdrawal

Withdrawing must be as easy as granting. The script cannot be unloaded once it
ran, so delete the consent cookie and reload. The next page view has no script,
no queue and no attribution. The old `datafast_visitor_id` cookie stays until it
expires, but nothing reads it any more.

## The cost, stated plainly

With consent required, you measure only people who clicked yes. Expect a large
drop in visitors and some revenue with no channel. That is the correct number
for your jurisdiction, not a bug to fix.

A reverse proxy does not change any of this. It hides a hostname from blocklists.
It is not a legal basis.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
