Consent — a headless consent state machine | PolicyStack

2 min read Original article ↗

[consent]sub-4kb · v1.0

Tiny core. Adapters for every major framework. A Vite plugin that yells at you when a script sets a cookie behind a category the user hasn’t accepted yet.

pnpm add @policystack/core @policystack/reactgithub

import { PolicyStack } from "@policystack/react/provider";
import { ConsentGate } from "@policystack/react/consent";
import config from "./policystack";

// One config. <PolicyStack> derives the consent categories
// (essential locked, analytics/marketing gated) from config.cookies —
// no separate categories array, no conversion step.
export function App() {
  return (
    <PolicyStack config={config}>
      <YourApp />
      <ConsentGate requires="analytics">
        <GoogleAnalytics />
      </ConsentGate>
    </PolicyStack>
  );
}

[01]what it does

Just the consent layer. Nothing else.

[01]

Headless core
A state machine that tracks categories, persists choices, and emits events. The UI is whatever you build.

[02]

Framework adapters
First-class hooks for React, Vue, Solid, Svelte, and Angular. Same store, same events, framework-idiomatic API.

[03]

Vite plugin
Watches for cookie writes during dev. Throws if a script sets a cookie behind a category the user hasn’t accepted.

[04]

Static scanner
CI step that scans built bundles for ungated cookie usage so things don’t regress between releases.

[05]

Integrations
GA, Meta Pixel, GTM, Hotjar, PostHog — load them gated behind the right consent category by default.

[06]

CLI (planned)
Bootstrap a config from your existing cookies, audit a deployed site, generate a per-environment policy.

[02]dev plugin

Catch leaky cookies before users do.

The Vite plugin patches document.cookie in dev and refuses writes that fall outside the categories the user has accepted — with a stack trace pointing at the line that did it.

vite dev — terminal

! consent violation

[policystack] ungated cookie write blocked
  cookie:    _ga
  category:  analytics  (not accepted)
  source:    src/lib/analytics.ts:18:5
  fix:       guard with consent.has("analytics")

[03]install

Two lines and you’re shipping.

pnpm add @policystack/core @policystack/react
import { useConsent } from "@policystack/react/consent";

export function CookieBanner() {
  const { acceptAll, acceptNecessary } = useConsent();
  return (
    <div>
      <button onClick={acceptAll}>Accept all</button>
      <button onClick={acceptNecessary}>Necessary only</button>
    </div>
  );
}

Already using Policy? Wire them together

[04]sponsor

Keep the core free, forever.

Consent and Policy are Apache-2.0 and will stay that way — no relicensing, no features held back behind a cloud tier. Sponsorship pays for the time it takes to keep both repos maintained, audited, and worth depending on.

sponsor on github