# PolicyStack > Privacy policies and headless cookie consent from one TypeScript config. V1 documentation for developers and coding agents. # Privacy and consent as code Source: https://policystack.dev/ PolicyStack V1 turns one TypeScript configuration into privacy and cookie policies, plus a headless consent store. Keep your disclosures and consent categories together in code; render them in your own application and design system. [Get started with V1](https://policystack.dev/docs/quickstart) · [Read the documentation](https://policystack.dev/docs) · [View the source](https://github.com/jamiedavenport/policystack) ## Start with your application ```sh pnpm dlx @policystack/cli@1 init ``` The CLI scaffolds a configuration and generates a reference for your coding agent. Review the company details, declared data, purposes, retention, and jurisdictions before publication. ## One config, two capabilities ```tsx import { PolicyStack } from "@policystack/react/provider"; import { PrivacyPolicy } from "@policystack/react/policy"; import { ConsentGate } from "@policystack/react/consent"; import policy from "./policystack"; export function PrivacyPage() { return (

Analytics consent has been granted.

); } ``` Use the [complete quickstart](https://policystack.dev/docs/quickstart) for the configuration and a working consent choice UI. - **Policy generation:** privacy and cookie documents, framework-native renderers, and Markdown, HTML, or PDF output. - **Headless consent:** your banner and preferences UI, backed by a shared store, jurisdiction posture, and explicit script gates. - **Development feedback:** source annotations and opt-in Vite scanning surface potential disclosure and consent gaps. - **Agent tooling:** generated SDK reference, CLI validation, MCP tools, and reusable skills. [Check framework support and limitations](https://policystack.dev/docs/reference/support). V1 packages are Apache-2.0. Generated documents require review; PolicyStack does not determine your legal obligations or guarantee compliance. ## What comes next V2 is in design. The direction is a self-hostable platform connecting privacy inventory, consent, backend enforcement, and rights workflows. These capabilities are not shipped V1 features. [Read the V2 roadmap](https://policystack.dev/docs/roadmap) · [Discuss becoming a design partner](mailto:jamie@policystack.dev) --- # PolicyStack V1 documentation Source: https://policystack.dev/docs PolicyStack V1 provides privacy and cookie policy generation, headless consent, static source scanning, and tools for coding agents. One `PolicyStackConfig` supplies disclosures and consent categories. Your application owns the UI and runtime integration. ## Get started 1. Follow the [complete React and TypeScript quickstart](https://policystack.dev/docs/quickstart). 2. Choose [policy rendering](https://policystack.dev/docs/policy) or [cookie consent](https://policystack.dev/docs/consent), independently or together. 3. Check the [framework support matrix and current limitations](https://policystack.dev/docs/reference/support). 4. Add [Vite diagnostics](https://policystack.dev/docs/consent/vite) and [CLI validation](https://policystack.dev/docs/policy/cli) to development and CI. ## Find an answer - [How do I generate a privacy policy?](https://policystack.dev/docs/policy/policies/quick-start) - [How do I add cookie consent to React?](https://policystack.dev/docs/consent/react) - [How do I configure company, data, and cookie declarations?](https://policystack.dev/docs/policy/configuration) - [How do I detect ungated analytics?](https://policystack.dev/docs/consent/vite) - [Which frameworks and output formats are supported?](https://policystack.dev/docs/reference/support) - [How do I connect a coding agent?](https://policystack.dev/docs/policy/agent-skills) - [What is planned for V2?](https://policystack.dev/docs/roadmap) ## For coding agents Use [llms.txt](https://policystack.dev/llms.txt) to discover current documentation, [llms-full.txt](https://policystack.dev/llms-full.txt) for the current corpus, and [sdk.txt](https://policystack.dev/sdk.txt) for the generated SDK reference. Append `.md` to a documentation URL to retrieve plain Markdown, including code examples. Treat historical blog posts as historical and the V2 roadmap as unshipped direction. ## Current product boundary PolicyStack V1 is a TypeScript-first library, not a hosted compliance service. It has no finished banner UI, self-hostable control plane, backend enforcement SDK, or data-subject request workflow. Generated documents and configured controls need human review. See the [support reference](https://policystack.dev/docs/reference/support) for exact coverage. --- # Consent Source: https://policystack.dev/docs/consent > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Open-source primitives for building cookie banners and preferences. **Consent logic, not consent UI.** Consent gives you a tiny, headless state machine and framework-native hooks for managing user consent. You write the banner. We handle the rules. ## Why? Most consent libraries ship a banner with the logic baked in. You either bend your design to match theirs or fight the library every step of the way. Consent takes the opposite approach. The state machine, expressions, storage, and script gating are all yours to use — the UI is whatever you build around them. ## Install ```sh npm install @policystack/core @policystack/react npm install @policystack/core @policystack/vue npm install @policystack/core @policystack/solid npm install @policystack/core @policystack/svelte npm install @policystack/core @policystack/angular ``` ## Quick start Follow the [complete React quickstart](https://policystack.dev/docs/quickstart) to define `policystack.ts`, mount the provider, and render working consent choices. The [React reference](https://policystack.dev/docs/consent/react) adds category controls and preferences. Category labels, descriptions, and GPC options come from `cookies.context`. Consent gates change only after a choice is committed; staged preferences do not change enforcement until saved. ## Features - **Headless** — no styles, no DOM, no opinions about how your banner looks - **Hooks-first** — same API across React, Vue, Solid, and Svelte, translated to native reactivity - **Pluggable storage** — localStorage, cookies, or your own server - **Jurisdiction-aware** — different defaults for EEA, UK, US states, and more - **Script gating** — load third-party tags only after consent, with pre-built integrations for GA4, Meta Pixel, PostHog, Segment, and others - **GPC support** — honours the Global Privacy Control signal out of the box - **Versioned consent records** — re-prompt automatically when your policy changes - **Vite plugin** — detects ungated cookie usage at build time and warns before you ship ## Vite plugin ```ts // vite.config.ts import { policyStack } from "@policystack/vite"; export default { plugins: [policyStack({ consent: { mode: "warn" } })], }; ``` One plugin covers both products. The opt-in `consent` option turns on the cookie scanner: it scans your code for cookie writes and known third-party vendors, and flags any that aren't behind a `ConsentGate` or `has()` check. ## Packages | Package | Description | | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) | Headless consent store, GPC handling, jurisdiction resolvers, script gating, storage adapters | | [`@policystack/react/consent`](https://policystack.dev/docs/consent/react) | React 18+ adapter — one `` provider, `useConsent`, `useCategory`, `` | | [`@policystack/vue/consent`](https://policystack.dev/docs/consent/vue) | Vue 3 adapter — one `` provider, composables, `` | | [`@policystack/solid`](https://policystack.dev/docs/consent/solid) | Solid adapter — one `` provider, signals-based hooks | | [`@policystack/svelte/consent`](https://policystack.dev/docs/consent/svelte) | Svelte 5 runes adapter (+ Svelte 5 `Readable` API at `/stores`) | | [`@policystack/angular`](https://policystack.dev/docs/consent/angular) | Angular 20+ adapter — `providePolicyStackConsent`, `ConsentService`, `injectCategory`, `*ocConsent` | | [`@policystack/vite`](https://policystack.dev/docs/consent/scanner) | Static AST detection of cookie writes and vendor scripts | | [`@policystack/vite`](https://policystack.dev/docs/consent/vite) | Vite plugin: surfaces ungated cookie / vendor calls in dev and CI | | [`@policystack/cli`](https://policystack.dev/docs/consent/cli) | Setup, validation, and MCP tools | | [`@policystack/scripts`](https://policystack.dev/docs/consent/scripts) | Pre-built script integrations: GA4, Meta Pixel, PostHog, Segment, GTM, Hotjar, Microsoft Clarity | Shared concepts (categories, GPC, jurisdiction, re-consent triggers, script gating, storage adapters) live in [`@policystack/core/consent`](https://policystack.dev/docs/consent/core); the framework adapters are thin wrappers over it. ## Companion to Policy Consent pairs with [Policy](https://policystack.dev/docs/policy) for the full privacy story: a single config drives your cookie banner, your cookie policy document, and your privacy policy disclosures. They work great together — and just as well apart. ## Status Stable as of 1.0 — the public surface (the consent store, expressions, and the slot contract) is frozen, and changes follow semver. Track progress on the [roadmap](https://github.com/jamiedavenport/policystack/issues). ## License [Apache-2.0](https://github.com/jamiedavenport/policystack/blob/main/LICENSE.md) --- # Add cookie consent to Angular Source: https://policystack.dev/docs/consent/angular > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Angular 20+ adapter for Consent. Bridges [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) with Angular's signal reactivity. ## Install ```sh pnpm add @policystack/core @policystack/angular ``` Peer dependencies: `@angular/core >= 18`, `@angular/common >= 18`. ## Setup Register the provider once at the root of your standalone application: ```ts import { bootstrapApplication } from "@angular/platform-browser"; import { providePolicyStackConsent } from "@policystack/angular/consent"; import { localStorageAdapter } from "@policystack/core/consent/storage/local-storage"; import { AppComponent } from "./app.component"; bootstrapApplication(AppComponent, { providers: [ providePolicyStackConsent({ config: { categories: [ { key: "essential", label: "Essential", locked: true }, { key: "analytics", label: "Analytics" }, { key: "marketing", label: "Marketing" }, ], adapter: localStorageAdapter(), }, }), ], }); ``` You can pass a pre-created store instead of `config`: ```ts import { createConsentStore } from "@policystack/core/consent"; const store = createConsentStore({ categories }); providePolicyStackConsent({ store }); ``` ## API ### `ConsentService` Inject `ConsentService` anywhere to read consent state via signals and trigger actions. ```ts import { Component, inject } from "@angular/core"; import { ConsentService } from "@policystack/angular/consent"; @Component({ selector: "app-banner", standalone: true, template: ` @if (consent.route() === "cookie") {
} `, }) export class BannerComponent { readonly consent = inject(ConsentService); } ``` Signal properties: `route`, `categories`, `decisions`, `draft`, `jurisdiction`, `policyVersion`, `decidedAt`, `repromptReason`, `state`. Methods: `acceptAll`, `acceptNecessary`, `reject`, `toggle`, `save`, `setRoute`, `has`, `getConsentRecord`, `getPreviousRecord`. ### `injectCategory(key)` Granular per-category access. Must be called inside an injection context (e.g. a component constructor or field initializer). `toggle` stages the change and `granted()` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, ``, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it. ```ts import { Component } from "@angular/core"; import { injectCategory } from "@policystack/angular/consent"; @Component({ selector: "category-row", standalone: true, template: ` `, }) export class CategoryRowComponent { readonly analytics = injectCategory("analytics"); } ``` ### `*ocConsent` directive Structural directive that conditionally renders content based on a consent expression. Mirrors `@policystack/react/consent`'s `` and `@policystack/vue/consent`'s ``. ```ts import { Component } from "@angular/core"; import { ConsentGate } from "@policystack/angular/consent"; import { ChartComponent } from "./chart.component"; import { EnablePromptComponent } from "./enable-prompt.component"; @Component({ selector: "gated-chart", standalone: true, imports: [ConsentGate, ChartComponent, EnablePromptComponent], template: ` `, }) export class GatedChartComponent {} ``` The directive emits no DOM wrapper — only the templated content (or its fallback) is rendered. ## SSR (Angular Universal) `providePolicyStackConsent` runs the store factory once per request injector on the server, which is the right scope for cookie/header-based jurisdiction or storage. Use `@policystack/core/consent/storage/server` for a server-side adapter seeded from the inbound request, and the local-storage / cookie adapters in the browser. See [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) for storage adapter details. ## Shared concepts Categories, GPC handling, jurisdiction resolvers, re-consent triggers, script gating (`gateScript`), and storage adapters all live in [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — the Angular adapter is a thin reactivity wrapper. ## See also - [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — shared concepts and config reference - [`@policystack/vite`](https://policystack.dev/docs/consent/vite) — build-time check for ungated cookie / vendor calls - [Other adapters](https://policystack.dev/docs/reference/support#which-frameworks-does-policystack-support) — React, Vue, Solid, Svelte ## License Apache-2.0 --- # Consent scanning with CLI and MCP tools Source: https://policystack.dev/docs/consent/cli PolicyStack V1's CLI implements `init`, `validate`, and `mcp`. Consent-specific `policystack scan` and `policystack sync` shell commands are not implemented. ## Validate your consent declarations ```sh pnpm add -D @policystack/cli@1 pnpm exec policystack validate --json ``` This validates configuration; it does not scan every runtime data flow. Review the returned diagnostic codes and update the configuration or application as required. ## Scan source for ungated analytics Use the [Vite integration](https://policystack.dev/docs/consent/vite) with consent scanning enabled, or configure a coding agent to run `pnpm exec policystack mcp` and call its `scan_ungated` tool. Both use the [static consent scanner](https://policystack.dev/docs/consent/scanner). Static findings are heuristic. Configure Vite's error mode to fail CI on findings. Runtime gating still requires the application's consent APIs. See the [CLI reference](https://policystack.dev/docs/policy/cli) for setup flags, validation, and MCP configuration. --- # @policystack/core/consent Source: https://policystack.dev/docs/consent/core > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Framework-agnostic consent store for Consent. Owns consent state and broadcasts changes to subscribers via a small pub/sub interface that each framework adapter wraps in its own reactivity primitive. If you're using a framework, install one of the adapters instead and read this for the shared concepts: [react](https://policystack.dev/docs/consent/react) · [vue](https://policystack.dev/docs/consent/vue) · [solid](https://policystack.dev/docs/consent/solid) · [svelte](https://policystack.dev/docs/consent/svelte). ## Install ```sh bun add @policystack/core ``` ## Quick start ```ts import { createConsentStore } from "@policystack/core/consent"; import { localStorageAdapter } from "@policystack/core/consent/storage/local-storage"; const store = createConsentStore({ categories: [ { key: "essential", label: "Essential", locked: true }, { key: "analytics", label: "Analytics" }, { key: "marketing", label: "Marketing" }, ], adapter: localStorageAdapter(), }); store.subscribe((state) => render(state)); store.acceptAll(); ``` The store's surface: `getState()`, `subscribe()`, `acceptAll()`, `acceptNecessary()`, `reject()`, `toggle(key)`, `save()`, `setRoute()`, `has(expr)`, `getConsentRecord()`, `getPreviousRecord()`, `refreshJurisdiction()`, `server`. See [`types.ts`](https://github.com/jamiedavenport/policystack/blob/main/packages/core/src/consent/types.ts) for the full shape. ### Server rendering (`store.server`) `getState()` and `has()` read live state, which varies with the environment: a server has no stored record, and `timezoneResolver` there resolves the **host's** timezone rather than the visitor's. Rendering from live state on the server therefore mismatches the client that hydrates it. `store.server.getState()` and `store.server.has(expr)` return the deterministic pre-consent view instead — undecided, no jurisdiction, conservative opt-in, derived from static config alone. Two stores built from one config agree here whatever adapter or resolver they were given. `getState()` is frozen and referentially stable, as React's `useSyncExternalStore` requires of `getServerSnapshot`. The React bindings wire this for you, so consent-driven UI hydrates cleanly with no `mounted` flag. Custom bindings should render from `store.server` on the server and during hydration, then switch to live state. ### Staged preferences (`state.draft`) `toggle(key)` never changes live consent. It stages the flip in `state.draft`, and gating (`has()` / ``), storage, and gated scripts keep reading `decisions` until `save()` promotes the draft in one step and stamps `decidedAt`. Leaving the preferences flow without saving — any `setRoute` that does not land on `"preferences"` — discards the draft, so "Back" genuinely abandons unsaved edits and nothing was loaded or persisted in the meantime. Render preference checkboxes from `draft ?? decisions` so the panel responds instantly; the framework bindings' per-category `granted` accessor does exactly this. ## Storage adapters Decisions persist via a `StorageAdapter` passed to `createConsentStore({ adapter })`. Three adapters ship as subpath imports: - `@policystack/core/consent/storage/local-storage` — browser localStorage. Subscribes to `storage` events for cross-tab sync. - `@policystack/core/consent/storage/cookie` — `document.cookie` with a configurable name, domain, and `Max-Age`. Survives subdomain navigation. - `@policystack/core/consent/storage/server` — header-based read + `Set-Cookie` write, for SSR runtimes. Implement the `StorageAdapter` interface (`read`, `write`, `clear`, optional `subscribe`) for anything else (IndexedDB, your own backend, etc.). ### Storage key The localStorage and cookie adapters both default to `ps_consent`, overridable with `localStorageAdapter({ key })` and `cookieAdapter({ name })`. Rather than hardcoding the name server-side, read it off the adapter: `cookieAdapter().name`. Before 1.3.0 the default was `oc_consent`, a leftover from the OpenCookies rebrand. Both adapters still **read** the old key when the new one is absent, so visitors who already decided are not re-prompted. The fallback is read-only — writes always use `ps_consent` — with one exception: `clear()` removes both, so withdrawing consent cannot be undone by the fallback. It is skipped entirely if you pass your own `key`/`name`. On the server, clear consent with `getSetCookieHeaders(null)`, which returns every `Set-Cookie` header you need to emit including the one expiring the legacy cookie. The singular `getSetCookieHeader` covers only the canonical cookie. ## Jurisdiction A `JurisdictionResolver` tells the store which region the visitor is in, so banner defaults can vary (opt-in for EEA/UK, opt-out for US, and so on). The resolved jurisdiction is stored on the consent record and persists across decision changes. ```ts import { createConsentStore, headerResolver } from "@policystack/core/consent"; // Edge runtime (Cloudflare, Vercel, Netlify): read country from request headers. const store = createConsentStore({ categories, jurisdictionResolver: headerResolver(), request, // standard Request, or anything with a Headers instance }); ``` Four resolvers ship today: - `headerResolver()` reads `cf-ipcountry`, `x-vercel-ip-country`, or `x-country` and normalises the country to a `Jurisdiction`. Best fit for edge runtimes (Cloudflare, Vercel, Netlify). - `timezoneResolver()` reads `Intl.DateTimeFormat().resolvedOptions().timeZone` and looks up the country via a bundled IANA → ISO map. Zero network, no IP leak. State-level US jurisdictions (`US-CA`, `US-CO`, …) are not derivable from IANA zones — `America/Los_Angeles` returns `"US"`, not `"US-CA"`. - `manualResolver(jurisdiction)` returns a fixed value — useful for tests and SSR overrides. - `clientGeoResolver({ endpoint })` `fetch`es a developer-provided endpoint that returns `{ country, region? }`. No IP database is bundled. There is no default resolver; if you omit `jurisdictionResolver`, `state.jurisdiction` stays `null` and any `gpc.applicableJurisdictions` filter that requires a known jurisdiction is treated as not matching. Call `store.refreshJurisdiction(req?)` to re-resolve (e.g. after client-side navigation in an SSR app). The resolver is otherwise called once per session and cached. ### Custom resolver Implement the `JurisdictionResolver` interface and reuse `countryToJurisdiction` for normalisation: ```ts import { type JurisdictionResolver, countryToJurisdiction } from "@policystack/core/consent"; export function ipApiResolver(): JurisdictionResolver { return { async resolve() { const res = await fetch("https://ipapi.co/json/"); const { country_code } = await res.json(); return countryToJurisdiction(country_code); }, }; } ``` ## Global Privacy Control [Global Privacy Control](https://globalprivacycontrol.org/) (GPC) is a browser signal asserting "do not sell or share". It is legally enforceable under California's CPRA and the consumer-privacy laws of Colorado, Connecticut, Virginia, and others. When GPC is asserted, the store sets `decisions` for opt-out categories to `false` and stamps `state.source = "gpc"`. GPC is treated as a _signal_, not a _decision_: `route` and `decidedAt` stay untouched so the banner remains visible and the user can still affirmatively consent (per the W3C GPC draft spec, an explicit user grant overrides the signal). Nothing is persisted to your storage adapter for GPC-only state — `getConsentRecord()` returns `null` until the user acts. The privacy-positive default applies GPC in every jurisdiction with no extra config: ```ts import { createConsentStore } from "@policystack/core/consent"; const store = createConsentStore({ categories }); // Brave (and any browser asserting GPC) starts with all opt-outs denied. ``` Once a user makes an explicit decision (`acceptAll`, `save`, etc.) the resulting record has `state.source === "user"` and is preserved on reload — `applyGPC` will not overwrite it. To scope GPC to the legally-required US states only: ```ts import { GPC_LEGALLY_REQUIRED_JURISDICTIONS, createConsentStore } from "@policystack/core/consent"; const store = createConsentStore({ categories, gpc: { applicableJurisdictions: GPC_LEGALLY_REQUIRED_JURISDICTIONS }, }); ``` The exported list is derived from the jurisdiction capability table and currently covers California, Colorado, Connecticut, Delaware, Maryland, Minnesota, Montana, Nebraska, New Hampshire, New Jersey, Oregon, and Texas. `clientGeoResolver` preserves all 50 US state codes, so visitors in those states match this scope directly. A category that should ignore GPC sets `respectGPC: false`: ```ts const categories = [ { key: "essential", label: "Essential", locked: true }, { key: "analytics", label: "Analytics", respectGPC: false }, { key: "marketing", label: "Marketing" }, ]; ``` To disable GPC handling entirely (e.g. you want to display GPC status yourself): ```ts createConsentStore({ categories, gpc: { enabled: false } }); ``` `state.source` is `"default"` before any decision, `"gpc"` after GPC applies, and `"user"` once the visitor takes any action. Persist this alongside the decisions to keep "the browser said no" distinct from "the user said no" later. ## Consent records When a decision is persisted via a `StorageAdapter`, the store serialises it as a versioned `ConsentRecord`: ```ts type ConsentRecord = { schemaVersion: 1; decisions: Record; policyVersion: string; decidedAt: string; // ISO-8601 jurisdiction: Jurisdiction | null; locale: string; source: "banner" | "preferences" | "api" | "import"; }; ``` `source` records _where_ the decision came from, separately from `state.source`: - `"banner"` — accepted/rejected from the cookie banner. - `"preferences"` — changed inside the preferences UI. - `"api"` — set via a programmatic call (override with `acceptAll({ source: "api" })`, etc.). - `"import"` — migrated from a legacy or unrecognised record. The store infers `source` from `state.route` at the moment the decision is taken; pass `{ source }` to any decision action (`acceptAll`, `acceptNecessary`, `reject`, `save`) to override it. `toggle` takes no options — it only stages a draft, and the eventual `save` names the source. Read the current record via `store.getConsentRecord()` (or the binding-level `useConsent().getConsentRecord()`). It returns `null` until a decision has been recorded. ```ts const store = createConsentStore({ categories, locale: "en-GB", // optional; falls back to navigator.language, then "en" adapter: cookieAdapter(), }); store.acceptAll(); store.getConsentRecord(); // { // schemaVersion: 1, // decisions: { essential: true, analytics: true, marketing: true }, // policyVersion: "", // decidedAt: "2026-04-29T12:34:56.000Z", // jurisdiction: "EEA", // locale: "en-GB", // source: "banner", // } ``` Records produced by older versions of Consent are tolerated on read: missing fields fall back to safe defaults, the legacy `source: "user"` flag is mapped to `"banner"`, and any other unrecognised legacy source becomes `"import"`. The next user decision rewrites the record in the v1 shape. GPC alone does not produce a record — the visitor has not made a decision. `getConsentRecord()` keeps returning `null` until the user accepts, rejects, or saves their preference changes. ## Re-consent triggers A stored `ConsentRecord` can become stale: the cookie policy is updated, a new category appears, the visitor moves to a different jurisdiction, or the record simply ages out. Pass a `triggers` config to declare when the store should re-prompt instead of restoring stored decisions. ```ts const store = createConsentStore({ categories, policyVersion: "v2", adapter: cookieAdapter(), triggers: { policyVersionChanged: true, // config.policyVersion !== record.policyVersion categoriesAdded: true, // a category in config is missing from the record expiresAfter: "13 months", // older than the duration → re-prompt jurisdictionChanged: true, // current jurisdiction differs from the recorded one }, }); ``` `expiresAfter` accepts: - a number of milliseconds (`60_000`); - a human-friendly string (`"13 months"`, `"30 days"`, `"1 year"`, `"24h"`, `"90s"`); - an ISO 8601 duration (`"P13M"`, `"P1Y"`, `"PT24H"`); - `null` or omitted to never expire. When any trigger fires, the store invalidates state — `route` returns to `"cookie"`, `decidedAt` is cleared, decisions reset to defaults — and exposes the original record on `state.repromptReason` and `store.getPreviousRecord()`: ```ts const { repromptReason, getPreviousRecord } = useConsent(); if (repromptReason !== null) { console.log(`Re-prompting because: ${repromptReason}`); console.log("Previous decisions:", getPreviousRecord()?.decisions); } ``` `repromptReason` is one of `"policyVersion" | "categoriesAdded" | "expired" | "jurisdiction"`, in priority order — the first trigger to fire wins. Once the visitor makes a new decision (`acceptAll`, `acceptNecessary`, `reject`, or `save`), `repromptReason` clears, `getPreviousRecord()` returns `null`, and a fresh record is written via the adapter. For analytics, the store emits a `policystack:reprompt` event on `globalThis` whenever a trigger fires, with `event.detail.reason` containing the trigger name: ```ts globalThis.addEventListener("policystack:reprompt", (event) => { analytics.track("consent_reprompt", { reason: event.detail.reason }); }); ``` ## Script gating Third-party tag scripts (GA4, Meta Pixel, PostHog, …) need to be loaded _only_ after the visitor consents to the matching category — but typical site code calls `window.gtag(…)` from the moment the page boots. `gateScript` solves that gap: it intercepts pre-consent calls to listed window globals, and once consent is granted it runs the vendor's snippet bootstrap (`init`), replays the queued calls into it, and then injects the ` {@render children?.()} ``` You can pass a pre-created store with `setPolicyStackConsentContext({ store })` instead. ## API ### `getConsent()` Returns a reactive object whose properties are tracked via `$state`. Read directly in markup — no destructuring required to keep reactivity. ```svelte {#if consent.route === "cookie"} {/if} ``` ### `getCategory(key)` Granular per-category access. `toggle` stages the change and `granted` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, ``, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it. ```svelte ``` ### `` Renders the `children` snippet when an expression is satisfied; renders `fallback` snippet otherwise. ```svelte {#snippet children()} {/snippet} {#snippet fallback()} {/snippet} {#snippet children()} {/snippet} ``` ### `` Consent-gates one third-party script against the store installed by `setPolicyStackConsentContext`. It is the intended way to use the [`@policystack/scripts`](https://policystack.dev/docs/consent/scripts) catalogue from Svelte. ```svelte ``` The component renders no DOM and gates from `$effect`, so it is inert during SSR. Definitions can be built inline: a fresh object with the same `def.id` does not restart the gate or discard queued calls. Changing the ID disposes the old gate and starts the new one. `onEvent` receives `script:gated`, `script:queued`, and `script:loaded` events. Core's [no-auto-revoke behavior](https://policystack.dev/docs/consent/core#no-auto-revoke) still applies: once loaded, a vendor script is not unloaded when consent changes or the component is destroyed. ## SvelteKit (SSR + hydration) Call `setPolicyStackConsentContext` from your root layout. It uses Svelte's `setContext`, so it hydrates safely: ```svelte {@render children()} ``` ## Svelte stores API When you prefer `$store` syntax in a Svelte 5 codebase, import from the `/stores` subpath: ```svelte {#if route === "cookie"} {/if} ``` `createConsentReadable` returns a `Readable` augmented with the same action methods as `getConsent()` (`acceptAll`, `toggle`, `save`, `has`, etc.). ## Shared concepts Categories, GPC handling, jurisdiction resolvers, re-consent triggers, script gating, and storage adapters all live in [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — the Svelte adapter is a thin reactivity wrapper. ## See also - [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — shared concepts and config reference - [`@policystack/vite`](https://policystack.dev/docs/consent/vite) — build-time check for ungated cookie / vendor calls - [Other adapters](https://policystack.dev/docs/reference/support#which-frameworks-does-policystack-support) — React, Vue, Solid ## License Apache-2.0 --- # Detect ungated analytics with the Vite plugin Source: https://policystack.dev/docs/consent/vite > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Vite plugin for Consent. Runs `@policystack/vite` against your source on dev start and on every HMR update, and surfaces ungated cookie writes / vendor calls as Vite warnings — or build failures. ## Install ```sh bun add -D @policystack/vite ``` ## Usage `@policystack/vite` exports a single `policyStack()` plugin that serves both products. The cookie scanner is opt-in via the `consent` option — pass it and the plugin scans your source for ungated cookie writes and vendor scripts in addition to its policy duties: ```ts // vite.config.ts import { defineConfig } from "vite"; import { policyStack } from "@policystack/vite"; export default defineConfig({ plugins: [ policyStack({ consent: { mode: "warn" }, }), ], }); ``` The categories the scanner checks against are derived from the `cookies` block of your `policystack.ts` — there is no separate categories array to maintain here. ## Options These are the keys of the plugin's `consent` option: | Option | Type | Default | Description | | --------- | ---------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `mode` | `"warn" \| "error" \| "off"` | `"warn"` in dev, `"error"` in build | Controls how findings are reported. `error` causes `vite build` to fail when ungated findings remain. `off` skips scanning. | | `include` | `string[]` | scanner default | Glob(s) of files to scan. | | `exclude` | `string[]` | scanner default | Glob(s) to exclude from the scan. | For custom rules or a custom vendor registry, call the scanner library directly — see [`@policystack/vite/consent`](https://policystack.dev/docs/consent/scanner). ## Modes - **`warn`** (dev default): prints findings via Vite's logger. Does not fail the dev server. - **`error`** (build default): same console output, plus throws at `buildEnd` if any ungated findings remain — so CI fails. - **`off`**: scanner does not run. ## Output Each ungated finding is printed as: ``` [policystack] ungated google-analytics (analytics) call via global at src/app.tsx:12:3 Rule: vendor-imports Fix: wrap call sites in or guard with store.has("category") Suppress: // consent-ignore-next-line ``` A summary line follows: `[policystack] N cookies, M vendors, K ungated`. ## HMR On every save, the plugin re-runs the scanner against the changed file only (no full project re-scan). Findings added or cleared by the edit are logged inline. The incremental path stays under 50 ms on typical files. ## Suppression Inherits the scanner's comment syntax: ```ts // consent-ignore-next-line gtag("event", "ad_view"); ``` Or per-file (must appear in the first 10 lines): ```ts // consent-ignore-file ``` ## Compatibility Compatible with Vite 5 and 6. Framework-agnostic — works with React, Vue, Svelte, SolidStart, SvelteKit, Astro, Nuxt 3, and Remix because the plugin only consumes file paths and source text. ## See also - [`@policystack/vite/consent`](https://policystack.dev/docs/consent/scanner) — underlying detection engine, suppression syntax, custom rules - [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — runtime store and `` / `has()` shapes the scanner looks for - [Framework adapters](https://policystack.dev/docs/reference/support#which-frameworks-does-policystack-support) — React, Vue, Solid, Svelte ## License Apache-2.0 --- # Add cookie consent to Vue Source: https://policystack.dev/docs/consent/vue > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). > **Vue 1.5.0 packaging limitation:** the published package omits `@policystack/vue/provider`. Provider examples below describe the repository implementation and require a release exporting `./provider`. Direct policy rendering with `config` remains available. [Support details](https://policystack.dev/docs/reference/support). Vue 3 adapter for Consent. Bridges [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) with Vue's reactivity via `shallowRef` and `computed`. ## Install ```sh bun add @policystack/core @policystack/vue ``` Peer dependencies: `vue >= 3.4`. ## Setup There is **one** provider. Wrap your app with `` from `@policystack/vue/provider` and pass it your whole `policystack.ts` config — it supplies both the policy context (`` / ``) and the consent store. The consent categories (and their locked vs. consent-gated state) are derived from `config.cookies`; there is no separate categories array, plugin, or conversion step. ```vue ``` `useConsent` / `useCategory` / `useConsentStore` / `` / `` (from `@policystack/vue/consent`) read the store from this same provider. A policy-only config (no `cookies`) provides no store, so a consent composable used under it throws — that is a configuration error, not a runtime state. ## API ### `useConsent()` Returns reactive refs for the current consent state plus action methods. Use it inside `setup()` or any ` ``` ### `useCategory(key)` Granular per-category access. Returns a `granted` computed and a `toggle` action. `toggle` stages the change and `granted` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, ``, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it. ```vue ``` ### `` Renders the default slot when an expression is satisfied, optionally a `fallback` slot otherwise. The component itself emits no DOM wrapper. ```vue ``` ### `` Consent-gates one third-party script against the store from ``. It is the intended way to use the [`@policystack/scripts`](https://policystack.dev/docs/consent/scripts) catalogue from Vue. ```vue ``` The component renders no DOM and starts its gate after mount, so it is inert during SSR. Definitions can be built inline: a fresh object with the same `def.id` does not restart the gate or discard queued calls. Changing the ID disposes the old gate and starts the new one. `onEvent` receives `script:gated`, `script:queued`, and `script:loaded` events. Core's [no-auto-revoke behavior](https://policystack.dev/docs/consent/core#no-auto-revoke) still applies: once loaded, a vendor script is not unloaded when consent changes or the component unmounts. ### `useConsentStore()` Returns the stable, non-reactive `ConsentStore` from `` for core free functions such as `gateScripts`. Keep using `useConsent`, `useCategory`, or `` for reactive UI. ```ts import { gateScripts } from "@policystack/core/consent"; import { useConsentStore } from "@policystack/vue/consent"; const store = useConsentStore(); const dispose = gateScripts(store, definitions); ``` Like the other consent composables, it throws outside `` or under a policy-only config. ## Options API The composables are usable from Options API via `setup()`: ```vue ``` ## Nuxt 3 Mount the single provider once around your app — e.g. in `app.vue` (or a layout): ```vue ``` ## Shared concepts Categories, GPC handling, jurisdiction resolvers, re-consent triggers, script gating, and storage adapters all live in [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — the Vue adapter is a thin reactivity wrapper. ## See also - [`@policystack/core/consent`](https://policystack.dev/docs/consent/core) — shared concepts and config reference - [`@policystack/vite`](https://policystack.dev/docs/consent/vite) — build-time check for ungated cookie / vendor calls - [Other adapters](https://policystack.dev/docs/reference/support#which-frameworks-does-policystack-support) — React, Solid, Svelte ## License Apache-2.0 --- # Introduction Source: https://policystack.dev/docs/policy > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Policy generates privacy policies and cookie policies from TypeScript config files. Instead of maintaining documents manually or copying templates, you describe your actual data practices in code and Policy renders them as components inside your app. ## What you can do with it - **Render as components** — drop `` or `` directly into your React, Vue, or Svelte app - **Auto-collect** — scan your source for `collecting()` and `thirdParty()` annotations at build time so the policy stays in sync with the code - **Pair with a consent banner** — the same config drives your [Consent](https://policystack.dev/docs/consent) banner and preferences panel, with no second config ## Get set up in one command ```sh bunx @policystack/cli init ``` The CLI installs the right packages for your stack, writes a starter `policystack.ts`, and prints a prompt you can paste into a coding agent (Claude Code, Cursor, etc.) to finish filling in your config from your codebase. See the [CLI page](https://policystack.dev/docs/policy/cli). ## Why policies-as-code Policy documents go stale. When you add a new third-party service, change your data retention period, or expand to a new jurisdiction, a static document won't reflect that unless someone remembers to update it. With Policy, your policy config lives next to your codebase — it can be reviewed in PRs, diffed in git, and re-rendered any time something changes. --- # AI skill pack Source: https://policystack.dev/docs/policy/agent-skills > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). `llms.txt` gives a coding agent the _facts_ about the SDK. The **skill pack** gives it the _procedures_ — the closed loops that compose the CLI, the provider, and the build-time scanner into repeatable workflows. It ships as a Claude Code plugin, versioned in the monorepo and generated from the same frozen SDK types as `llms.txt`, with drift tests checking the committed generated files. Review generated suggestions against the installed package version. Policy generates policy documents; it is not legal advice. Have a lawyer review your policies before publication. ## Install In Claude Code: ``` /plugin marketplace add jamiedavenport/policystack /plugin install policystack ``` The four skills then activate automatically when a task matches. ## The skills - **policystack-init** — scaffold Policy in a project: run `@policystack/cli init`, then wire the single `` provider (it supplies both the policy context and the consent store from one config). - **policystack-audit** — the closed loop: run `policystack validate --json`, explain each issue code against the frozen 1.0 diagnostic surface, propose a minimal config fix, and re-validate until the config is clean. - **policystack-jurisdiction** — explain the consent-model and policy-text posture implied by a declared `jurisdictions` set, read straight from the canonical jurisdiction table. - **policystack-instrument** — find un-annotated data collection and data egress in a codebase and add `collecting()` / `sharing()` / `thirdParty()` / `defineCookie()` call sites so the generated policy matches reality. ## Generated references and drift checks Every enumeration a skill cites — jurisdiction ids, lawful bases, the issue codes `validate()` emits — is rendered from the live `@policystack/core` / `@policystack/sdk` tables at generation time and snapshotted with a drift test that fails the build if the generated pack and the shipped files disagree. The same mechanism backs [SDK reference](https://policystack.dev/sdk.txt). Removing or renaming a frozen code is a loud test failure, not a silently stale skill. You can also point any other agent at the local reference: `policystack init` writes `policystack.llms.txt` into your project — see the [CLI page](https://policystack.dev/docs/policy/cli). --- # PolicyStack CLI — init, validate, and MCP Source: https://policystack.dev/docs/policy/cli > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). `@policystack/cli` sets up Policy in your project. Run it once — it installs the right packages for your stack, scaffolds a starter `policystack.ts`, and prints a prompt you can paste into a coding agent (Claude Code, Cursor, etc.) to finish filling in your config from your codebase. ## Run it From the root of your project: ```sh bunx @policystack/cli init ``` The CLI also supports validation and an MCP server; keep it installed when you use those development workflows. ## What it does 1. **Detects your package manager** from lockfiles (`bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`) or the `packageManager` field in `package.json`. Falls back to `npm`. 2. **Detects frameworks** by reading your `package.json` dependencies and installs the matching Policy integration: - `vite` → `@policystack/vite` (devDependency) - `react` → `@policystack/react` - `vue` → `@policystack/vue` - `svelte` → `@policystack/svelte` - `@policystack/sdk` is always installed. 3. **Writes a starter `policystack.ts`** to `src/policystack.ts` if a `src/` directory exists, otherwise to the project root. 4. **Prints an agent prompt** between delimiters so you can copy it into a coding agent and have the rest of your config filled in automatically from your codebase. ## Flags | Flag | Default | Description | | ----------------------------- | ------------- | ----------------------------------------------- | | `--cwd ` | `.` | Working directory | | `--pm ` | auto-detected | Override package-manager detection | | `--skip-install` | `false` | Skip installation; still write config/reference | | `--dry-run` | `false` | Show planned actions without executing | | `--yes`, `-y` | `false` | Skip the confirmation prompt | | `--out ` | auto-detected | Output path for the starter `policystack.ts` | | `--force` | `false` | Overwrite an existing `policystack.ts` | ## Why a prompt instead of a wizard? A coding agent reading your codebase can help draft `data.collected`, `data.context`, `thirdParties`, `jurisdictions`, and cookie usage more accurately than a series of prompts ever could — it infers from your ORM schemas, imports, environment variables, and existing legal copy. The CLI gives you the scaffolding, the agent supplies the content. See [Configuration](https://policystack.dev/docs/policy/configuration) for the shape of `policystack.ts` and [Auto-collect](https://policystack.dev/docs/policy/policies/auto-collect) for declaring data collection inline in your source. ## Validate a config ```sh pnpm add -D @policystack/cli@1 pnpm exec policystack validate --json ``` Review the structured diagnostics and fix the indicated declarations. Type checking and validation do not determine whether your disclosures are legally adequate. ## Connect MCP tools ```sh pnpm exec policystack mcp ``` Configure your coding agent to launch that command as a stdio MCP server from the application directory. It exposes `validate_config`, `scaffold_config`, `explain_jurisdiction`, `list_data_categories`, `explain_issue`, and `scan_ungated`. See the [generated SDK reference](https://policystack.dev/sdk.txt) and [agent workflows](https://policystack.dev/docs/policy/agent-skills). Implementation: [CLI commands](https://github.com/jamiedavenport/policystack/blob/main/packages/cli/src/index.ts), [MCP tool registry](https://github.com/jamiedavenport/policystack/blob/main/packages/cli/src/mcp/tools.ts). --- # Configuration Source: https://policystack.dev/docs/policy/configuration > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). All policies are defined in a single config file using `defineConfig()` from `@policystack/sdk`. You can place it anywhere in your project. ## Install The fastest way is to run [`@policystack/cli`](https://policystack.dev/docs/policy/cli), which installs `@policystack/sdk` plus the right framework integration for your stack and scaffolds a starter config: ```sh bunx @policystack/cli init ``` Or install manually: ```sh bun add @policystack/sdk ``` ## Create your config ```ts // policystack.ts import { ContractPrerequisite, defineConfig, LegalBases, Voluntary } from "@policystack/sdk"; export default defineConfig({ company: { name: "Acme Inc.", legalName: "Acme Corporation", address: "123 Main St, Springfield, USA", contact: { email: "privacy@acme.com" }, }, effectiveDate: "2026-01-01", jurisdictions: ["eea", "us-ca"], data: { collected: { "Account Information": ["Name", "Email address"], "Usage Data": ["Pages visited", "IP address"], }, context: { "Account Information": { purpose: "To authenticate users and send service notifications", lawfulBasis: LegalBases.Contract, retention: "Until account deletion", provision: ContractPrerequisite("We cannot create or operate your account."), }, "Usage Data": { purpose: "To understand product usage and improve the service", lawfulBasis: LegalBases.LegitimateInterests, retention: "90 days", provision: Voluntary("None — your service is unaffected."), }, }, }, thirdParties: [], cookies: { used: { essential: true, analytics: false, marketing: false }, context: { essential: { lawfulBasis: LegalBases.LegalObligation }, analytics: { lawfulBasis: LegalBases.Consent }, marketing: { lawfulBasis: LegalBases.Consent }, }, }, automatedDecisionMaking: [], }); ``` The `company` block is shared across policy types. Supply company identity and contact values explicitly. V1 does not read host `package.json` metadata: omitted names and emails normalize to empty strings and validation reports them. `effectiveDate` and `jurisdictions` also apply to all policies. A `data` or `children` block triggers privacy emission, and `cookies` triggers cookie emission; `trackingTechnologies` alone does not. An explicit `policies` selection overrides automatic detection. ### Contact methods `company.contact` is an object: `email` is required, and `phone` is optional. The phone number is rendered alongside the email in the privacy and cookie policy contact sections. ```ts company: { // ... contact: { email: "privacy@acme.com", phone: "+1-800-555-0100", // optional }, }, ``` Setting `phone` is recommended when `jurisdictions` includes `us-ca`. CCPA §1798.130(a)(1) requires businesses to provide two or more designated methods for consumers to submit privacy requests, and (unless you operate exclusively online) one of those methods must be a toll-free number. When `phone` is set, the rendered CCPA supplement appends a "Submitting requests" block listing both methods. Omitting it under `us-ca` emits a validation warning. The `data` block has two sibling maps: `collected` (category → field labels) and `context` (category → metadata about that category). `defineConfig`'s generic enforces that every key in `collected` has a matching `context` entry with `purpose`, `lawfulBasis`, `retention`, and `provision`. The `cookies` block mirrors the same shape: `cookies.used` lists the categories you enable (with `essential: true` always required), and `cookies.context` declares the Article 6 basis for each enabled category. Cookie context entries can also provide `label`, `description`, and `respectGPC`; these flow into the consent categories exposed by the framework bindings, with missing copy resolved from the built-in dictionary for the configured locale. ### Data Protection Officer If you operate under GDPR or UK-GDPR, set `company.dpo` so the policy discloses DPO contact details as required by Article 13(1)(b): ```ts company: { name: "Acme Inc.", legalName: "Acme Corporation", address: "123 Main St, Springfield, USA", contact: { email: "privacy@acme.com" }, dpo: { email: "dpo@acme.com", name: "Jane Doe", // optional phone: "+1 555 010 2030", // optional address: "123 Main St...", // optional }, }, ``` If appointing a DPO is not required for your processing activities (see GDPR Article 37(1)), say so explicitly — the policy will include the disclosure in the GDPR/UK-GDPR supplements: ```ts company: { // ... dpo: { required: false, reason: "Our processing is not large-scale or systematic." }, }, ``` Omitting `dpo` emits a validation warning when `jurisdictions` includes `eea` or `uk`. ### Automated decision-making and profiling GDPR Article 13(2)(f) requires you to disclose whether you use automated decision-making or profiling (Article 22) — even an explicit "we don't" is required. Set `automatedDecisionMaking: []` to declare none, or list each activity with its `name`, `logic`, and `significance`: ```ts automatedDecisionMaking: [ { name: "Fraud scoring", logic: "Transactions are scored by a rules engine combining device fingerprint and historical patterns.", significance: "A high score may delay or decline a transaction; you can request human review.", }, ], ``` Omitting the field entirely emits a validation warning under EU/UK jurisdictions. When at least one activity is listed, the rendered policy automatically appends the Article 22(3) right-to-human-review paragraph referencing `company.contact`. `data.collected` is a map of category label → fields. `data.context[category]` carries the per-category metadata: `purpose` (prose describing _why_ you process it — GDPR Article 13(1)(c)), `lawfulBasis` (the Article 6 basis), `retention` (how long you keep it), and `provision` (whether providing it is statutory, contractual, a contract-prerequisite, or voluntary, plus the consequences of failing to provide it — GDPR Article 13(2)(e)). The provision helpers `Statutory()`, `Contractual()`, `ContractPrerequisite()`, and `Voluntary()` from `@policystack/sdk` build the right shape from a consequences string. Every key in `data.collected` must appear in `data.context`; `defineConfig` enforces this at type-check time, and the `policyStack()` Vite plugin re-validates it at build time (see [Build-time validation](#build-time-validation)). When auto-collect is enabled, the plugin also emits `policystack.gen.ts` alongside your config (check it in) so the same constraint applies to scanned `collecting()` categories even without running Vite first. The user rights you're legally required to disclose (access, erasure, portability, etc.) are derived automatically from `jurisdictions` — declare `eea` or `uk` and you get the six GDPR rights, declare `us-ca` and you get the four CCPA rights, declare any combination and you get the union. There's no `userRights` field to set. See [Supported jurisdictions](https://policystack.dev/docs/policy/references/jurisdictions) for the full list of codes. ### Policy versions `defineConfig()` hashes the resolved config and exposes `privacyVersion` and `cookieVersion` on the returned object — an 8-character FNV-1a hex string per document. The two hashes are scoped to the slice of the config that feeds each policy, so a privacy-only edit (e.g. adding an entry to `automatedDecisionMaking`) does not invalidate `cookieVersion`, and a cookie-only change does not invalidate `privacyVersion`. ```ts import policy from "./policy"; policy.privacyVersion; // "a1b2c3d4" policy.cookieVersion; // "f5e6d7c8" ``` When set, the version is rendered inline with the effective-date sentence in each policy's intro — `… Effective Date: 2026-01-01. Version: a1b2c3d4.` — so customers have a printed reference they can quote. Pin a manual version (e.g. for a published v3 doc) by passing it on input: ```ts defineConfig({ // ... privacyVersion: "v3", }); ``` Explicit values always win over the auto-computed hash. Both helpers — `computePrivacyVersion(config)` and `computeCookieVersion(config)` — are also re-exported from `@policystack/sdk` for callers building configs without `defineConfig`. `cookieVersion` also feeds the Consent bridge — see the [Consent docs](https://policystack.dev/docs/consent) — so a change to `cookies` (which also drives the derived consent mechanism) re-prompts consent automatically. ### Build-time validation The `policyStack()` Vite plugin loads your resolved `policystack.ts` and runs every validator in `@policystack/core` against it on each build. It catches issues that TypeScript can't — missing GDPR lawful bases, retention periods, CCPA contact methods, DPO disclosures, and similar requirements that depend on `jurisdictions` rather than on the static shape of the config. In `vite build`, validation **errors** abort the build with a non-zero exit code and a list of `[policystack] code: message` lines. Warnings are surfaced via Rollup's warning channel and do not block. In `vite dev`, both errors and warnings stream to the dev-server logger and never crash HMR — fix them as you go and the next save replays validation. Validation runs against the _resolved_ config, with auto-collected data shimmed in first — so a scanned `collecting()` category without a matching `data.context` entry will fail validation just as a hand-written one would. To opt out (for instance when you want only the type-level guarantees and the auto-collect virtual module): ```ts // vite.config.ts policyStack({ validate: false }); ``` ## Using AI The fastest way to fill out your config is to hand it to a coding agent. [`@policystack/cli`](https://policystack.dev/docs/policy/cli) prints a ready-made prompt for this — run `bunx @policystack/cli init`, paste the prompt into Claude Code or Cursor, and the agent will fill in `data`, `thirdParties`, `jurisdictions`, and cookie usage from your codebase. Agents are good at this because the config is typed and the fields map directly to things already described in your dependencies, environment variables, data models, and existing legal copy. --- # Internationalization Source: https://policystack.dev/docs/policy/i18n > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Policy emits around 125 strings into every compiled policy — headings, table headers, GDPR/CCPA boilerplate, formatted dates. Set a `locale` and those strings switch language. Your company name, processing purposes, retention text, and third-party descriptions pass through as you wrote them. ## Supported locales | Locale | Tag | Status | | ------- | ---- | ------------- | | English | `en` | Ships in v1.0 | | French | `fr` | Ships in v1.0 | | German | `de` | Ships in v1.0 | | Dutch | `nl` | Ships in v1.0 | | Spanish | `es` | Ships in v1.0 | English is the ground truth — every other dictionary is checked against it at compile time via the `Dictionary` type, so a missing key fails `tsc` rather than silently falling back to English at runtime. ## Setting a locale on the config Pass `locale` to `defineConfig` and every emitted string in both the privacy and cookie policies renders in that language: ```ts // policystack.ts import { ContractPrerequisite, defineConfig, LegalBases } from "@policystack/sdk"; export default defineConfig({ company: { name: "Acme, Inc.", legalName: "Acme, Inc.", address: "123 Main St, San Francisco, CA", contact: { email: "privacy@acme.com" }, }, effectiveDate: "2026-01-01", jurisdictions: ["eea"], locale: "fr", data: { collected: { "Account Information": ["Name", "Email"], }, context: { "Account Information": { purpose: "To authenticate users and send service notifications.", lawfulBasis: LegalBases.Contract, retention: "Until account deletion", provision: ContractPrerequisite("We cannot operate your account."), }, }, }, }); ``` `locale` is the rendering language; it is independent of `jurisdictions` (which is the regulatory surface). `locale` is optional and defaults to `"en"`. ## Per-render override (React) `` and `` accept a `locale` prop that overrides `config.locale` at render time. The same config can drive multiple languages side-by-side: ```tsx import { PolicyStack } from "@policystack/react/provider"; import { PrivacyPolicy } from "@policystack/react/policy"; import policy from "@/policy"; export function PrivacyPolicyPage() { return ( {/* uses config.locale */} {/* override → French */} ); } ``` Useful for multilingual sites that want a language switcher, or for serving an English fallback alongside a regional translation. ## Dates `effectiveDate` renders through `Intl.DateTimeFormat` with the locale's BCP-47 tag (`en-US`, `fr-FR`, `de-DE`, `nl-NL`, `es-ES`), pinned to UTC so the same input produces the same output across build servers in any timezone. The string `"2026-01-01"` becomes: - English — `January 1, 2026` - French — `1 janvier 2026` - German — `1. Januar 2026` - Dutch — `1 januari 2026` - Spanish — `1 de enero de 2026` ## What does not translate A handful of strings stay English by design — they're not user-facing policy content: - **`reason:` audit metadata** on heading nodes (e.g. `"Required by GDPR Article 13(1)(c)"`) — threaded into the document tree for compliance tooling, not rendered to end users. - **Validation messages** from `validate.ts`, `validate-config.ts`, and `validate-cookie.ts` — surface in build logs to the developer integrating Policy, not in the published document. - **Section IDs** (`"introduction"`, `"data-collected"`, …) — stable identifiers used by tests and framework integrations to target sections. - **Renderer format output** (markdown syntax, HTML tags, PDF bullet glyphs) — already locale-agnostic. - **Internal `Error` messages** thrown when configs are malformed — developer-facing. ## Versions are per-locale `locale` feeds into both `computePrivacyVersion` and `computeCookieVersion`, so a French build and an English build of the same config produce distinct 8-character version hashes. This is intentional: the Consent bridge re-prompts consent when the cookie-policy version changes, so users see a fresh prompt the first time they're served a different-language policy. See the [Consent docs](https://policystack.dev/docs/consent). ## Compliance caveat The translated GDPR/CCPA/UK-GDPR boilerplate is first-pass legal text. Have a native-speaking compliance reviewer or counsel sign off on the rendered output before relying on a non-English locale in production — the same posture as Policy's English output, where the policy is a document, not legal advice. ## See also - [Configuration](https://policystack.dev/docs/policy/configuration) — the full `defineConfig` reference, including how `locale` interacts with `effectiveDate` and `jurisdictions`. - [Adding a locale](https://github.com/jamiedavenport/policystack/blob/main/packages/core/src/i18n/README.md) — internal contributor guide for adding new languages. --- # Auto-collect Source: https://policystack.dev/docs/policy/policies/auto-collect > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Auto-collect scans your source files at build time and populates the `data.collected` and `thirdParties` fields of your privacy policy automatically — no need to keep those arrays up to date by hand. You still write `data.context` by hand (one entry per category, with `purpose`, `lawfulBasis`, `retention`, and `provision`); for scanned categories, `defineConfig` requires matching entries via a generated `policystack.gen.ts` (written next to your `policystack.ts` and meant to be committed). The same machinery covers cookie categories — scanned `cookies.used` keys must each appear in `cookies.context`. It works through two complementary mechanisms: - **`collecting()`** — a zero-cost wrapper you place around data storage calls to declare what you're storing - **`thirdParty()`** — a side-effect-free call you place next to third-party SDK initialisation to declare an external service The `@policystack/vite` plugin scans your source files at build time, extracts these declarations, and exposes them to `@policystack/sdk` at runtime so they render inside your policy. ## Install ```sh bun add -D @policystack/vite ``` ## Setup Add `policyStack()` to your Vite plugin array. The scan runs during `buildStart` and refreshes on change in dev. ```ts // vite.config.ts import { defineConfig } from "vite"; import { policyStack } from "@policystack/vite"; export default defineConfig({ plugins: [policyStack()], }); ``` ## `collecting()` Wrap any call that stores personal data with `collecting()`. It returns the second argument unchanged at runtime, so it composes naturally with ORM insert calls and similar patterns. ```ts import { collecting } from "@policystack/sdk"; export async function createUser(name: string, email: string) { return db.insert(users).values( collecting( "Account Information", // category — appears as a section heading in the policy { name, email }, // value — returned unchanged; matches your ORM schema { name: "Name", email: "Email address" }, // labels — human-readable names used in the policy ), ); } ``` **Arguments:** | Position | Name | Description | | -------- | ---------- | ---------------------------------------------------------- | | 1 | `category` | Policy section heading (e.g. `"Account Information"`) | | 2 | `value` | The value being stored — returned as-is at runtime | | 3 | `labels` | Object mapping field names to human-readable policy labels | **Constraints:** - The `category` string and all label **values** must be string literals. Dynamic values (variables, template literals) are silently skipped by the analyser. - Every key of `value` must appear in the label record. To exclude a field from the policy — for example an internal column like `hashedPassword` — use the `Ignore` sentinel re-exported from `@policystack/sdk`. - Multiple `collecting()` calls with the same category are merged; duplicate labels are deduplicated. ### Excluding sensitive fields with `Ignore` ```ts import { collecting, Ignore } from "@policystack/sdk"; export async function createUser(name: string, email: string, hashedPassword: string) { return db.insert(users).values( collecting( "Account Information", { name, email, hashedPassword }, { name: "Name", email: "Email address", hashedPassword: Ignore, // excluded from the compiled policy }, ), ); } ``` Using `Ignore` forces each exclusion to be explicit, so a reviewer can see at a glance which fields are intentionally hidden from the policy. ## `thirdParty()` Call `thirdParty()` next to third-party SDK initialisation to declare an external service. This is a no-op at runtime. ```ts import { thirdParty } from "@policystack/sdk"; import { PostHog } from "posthog-js"; thirdParty( "PostHog", // service name "Product analytics", // purpose — appears in the policy "https://posthog.com/privacy", // URL to the service's own privacy policy ); export const posthog = new PostHog(process.env.POSTHOG_KEY); ``` **Arguments:** | Position | Name | Description | | -------- | ----------- | -------------------------------------------- | | 1 | `name` | Service name as it appears in the policy | | 2 | `purpose` | Short description of why you use the service | | 3 | `policyUrl` | URL to the service's own privacy policy | **Constraints:** - All three arguments must be string literals. Dynamic values are silently skipped. - If multiple `thirdParty()` calls declare the same `name`, the first one (alphabetically by file path) wins. ## NPM package auto-detection Instead of writing `thirdParty()` calls manually, you can enable `usePackageJson` to detect known third-party services from your `package.json` dependencies automatically. ```ts // vite.config.ts policyStack({ thirdParties: { usePackageJson: true, }, }); ``` The plugin reads both `dependencies` and `devDependencies` from your project root `package.json` and matches against a built-in registry of known packages. Explicit `thirdParty()` calls always take precedence — `usePackageJson` only adds entries not already declared in source. **Known packages:** | npm package | Service | Purpose | | ----------------------------------------------------------------------------------- | ---------------- | ---------------------- | | `stripe`, `@stripe/stripe-js` | Stripe | Payment processing | | `braintree`, `@braintree/browser-drop-in` | Braintree | Payment processing | | `@sentry/browser`, `@sentry/node`, `@sentry/nextjs`, `@sentry/react`, `@sentry/vue` | Sentry | Error tracking | | `@datadog/browser-rum`, `dd-trace` | Datadog | Monitoring | | `posthog-js`, `posthog-node` | PostHog | Product analytics | | `mixpanel-browser` | Mixpanel | Product analytics | | `@segment/analytics-next` | Segment | Customer data platform | | `@amplitude/analytics-browser`, `amplitude-js` | Amplitude | Product analytics | | `@vercel/analytics` | Vercel Analytics | Web analytics | | `plausible-tracker` | Plausible | Web analytics | | `logrocket` | LogRocket | Session recording | | `@hotjar/browser` | Hotjar | Session recording | | `resend` | Resend | Transactional email | | `@sendgrid/mail` | SendGrid | Transactional email | | `intercom-client`, `@intercom/messenger-js-sdk` | Intercom | Customer messaging | ## The generated module Commit `policystack.gen.ts`. It carries the scanned values and the type augmentation that makes `defineConfig` demand a `data.context` / `cookies.context` entry per scanned key, so committing it keeps both live in CI without running Vite first. Since it is committed, it lands under whatever formatter you run — and the plugin's output style will not match every formatter. So the plugin only rewrites it when the scan actually changed. The header carries a digest of the scanned content: ```ts // AUTO-GENERATED by @policystack/vite — do not edit. (scan: 66861eea20c06b94) ``` Each build compares that digest against the current scan and skips the write when they match, however the file has been reformatted in the meantime. So you can format it like any other source file without builds fighting you. A missing or unreadable header always rewrites — delete the file to regenerate it. ## Plugin options ```ts policyStack({ srcDir: "src", // directory to scan extensions: [".ts", ".tsx"], // file extensions to include ignore: ["generated"], // extra directory names to skip thirdParties: { usePackageJson: true, // detect services from package.json }, }); ``` | Option | Type | Default | Description | | ----------------------------- | ---------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `srcDir` | `string` | `"src"` | Directory walked for `collecting()` calls, relative to the Vite project root | | `extensions` | `string[]` | `[".ts", ".tsx"]` | File extensions scanned | | `ignore` | `string[]` | `[]` | Extra directory names skipped during the walk (appended to built-in defaults: `node_modules`, `dist`, `.git`, `.next`, `.output`, `.svelte-kit`, `.cache`) | | `thirdParties.usePackageJson` | `boolean` | `false` | Detect third-party services from `package.json` dependencies | --- # Cookie Policy Source: https://policystack.dev/docs/policy/policies/cookies > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). See the [Quick Start](https://policystack.dev/docs/policy/policies/quick-start) to add a cookie policy page to your app. Add cookie fields to your config — the cookie policy is auto-detected from the presence of the `cookies` field: ```ts // policystack.ts import { defineConfig, LegalBases } from "@policystack/sdk"; effectiveDate: "2026-01-01", jurisdictions: ["eea", "us-ca"], cookies: { used: { essential: true, analytics: true, functional: false, marketing: false, }, context: { essential: { lawfulBasis: LegalBases.LegalObligation }, analytics: { lawfulBasis: LegalBases.Consent, label: "Analytics", description: "Helps us understand how the site is used.", respectGPC: true, }, functional: { lawfulBasis: LegalBases.Consent }, marketing: { lawfulBasis: LegalBases.Consent }, }, }, thirdParties: [ { name: "Google Analytics", purpose: "Website analytics and performance monitoring", policyUrl: "https://policies.google.com/privacy", }, ], ``` The consent mechanism (banner / preference panel / withdrawal) is **derived** from this cookie posture — any consent-gated category yields all three — so it is no longer authored. It surfaces in the cookie policy's consent section automatically. `cookies.used` always requires `essential: true`; other keys are `boolean` and act as additional categories. Every key in `cookies.used` must have a matching Article 6 basis in `cookies.context[key].lawfulBasis` — `defineConfig` enforces this at type-check time, and the rendered "Cookies and Tracking" section appends the basis to each enabled category. Each context entry may also set `label`, `description`, and `respectGPC` for the derived consent category. Missing copy falls back field-by-field to the built-in cookie-type dictionary for `locale` (English by default), so a preference panel can render `useConsent().categories` directly. Set `respectGPC: false` only for a category that should remain available when a GPC signal is active. `defineConfig` also computes a `cookieVersion` — an 8-char hash of the cookie slice of your config — which is printed in the intro paragraph next to the effective date. See [Policy versions](https://policystack.dev/docs/policy/configuration#policy-versions). Then render it: ```tsx import { PolicyStack } from "@policystack/react/provider"; import { CookiePolicy } from "@policystack/react/policy"; import policy from "@/policy"; export function CookiePolicyPage() { return ( ); } ``` Looking to add a consent banner? The same `cookies` config drives it — see the [Consent docs →](https://policystack.dev/docs/consent). --- # Overview Source: https://policystack.dev/docs/policy/policies/overview > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Policy supports two policy types, rendered independently from a single flat config. | Policy | Detected from | | -------------- | ------------------ | | Privacy Policy | `data`, `children` | | Cookie Policy | `cookies` | Each policy is optional — Policy auto-detects which to produce based on the fields you provide. The `company` block and shared fields (`effectiveDate`, `jurisdictions`) live at the top level and apply to every policy rendered. --- # Privacy Policy Source: https://policystack.dev/docs/policy/policies/privacy > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). See the [Quick Start](https://policystack.dev/docs/policy/policies/quick-start) to add a privacy policy page to your app. Add the `data` block to your config — the privacy policy is auto-detected from its presence. User rights (access, erasure, portability, etc.) are derived automatically from your `jurisdictions`: ```ts // policystack.ts import { ContractPrerequisite, defineConfig, LegalBases, Voluntary } from "@policystack/sdk"; effectiveDate: "2026-01-01", jurisdictions: ["eea", "us-ca"], data: { collected: { "Account Information": ["Name", "Email address"], "Usage Data": ["Pages visited", "IP address"], }, context: { "Account Information": { purpose: "To authenticate users and send service notifications", lawfulBasis: LegalBases.Contract, retention: "Until account deletion", provision: ContractPrerequisite("We cannot create or operate your account."), }, "Usage Data": { purpose: "To understand product usage and improve the service", lawfulBasis: LegalBases.LegitimateInterests, retention: "90 days", provision: Voluntary("None — your service is unaffected."), }, }, }, thirdParties: [], automatedDecisionMaking: [], ``` Set `automatedDecisionMaking: []` to declare that you don't use automated decision-making or profiling (GDPR Art. 13(2)(f) / Art. 22). To declare activities, list each with `name`, `logic`, and `significance` — see [Configuration](https://policystack.dev/docs/policy/configuration#automated-decision-making-and-profiling). `data.collected` lists the field labels per category, and `data.context[category]` carries the metadata: `purpose`, `lawfulBasis`, `retention`, and `provision`. Every category in `data.collected` must have a matching `context` entry — `defineConfig` enforces this at type-check time, and the `policyStack()` Vite plugin re-validates it at build time. The renderer joins them into a single Article 13(1)(c) line per category: **Account Information** — used for [purpose] — [Article 6 basis], and emits a separate Article 13(2)(e) section disclosing whether each category is required, contractual, a contract-prerequisite, or voluntary, with the consequences of refusal. With auto-collect, the plugin emits `policystack.gen.ts` alongside your config — commit it so the same constraint applies to scanned categories in CI. `data.collected` and `thirdParties` can also be populated automatically — see [Auto-collect](https://policystack.dev/docs/policy/policies/auto-collect). `defineConfig` computes a `privacyVersion` — an 8-char hash scoped to the privacy slice of your config — which is printed in the intro paragraph next to the effective date. Edits to cookie-only fields do not invalidate it. See [Policy versions](https://policystack.dev/docs/policy/configuration#policy-versions). Then render it: ```tsx import { PolicyStack } from "@policystack/react/provider"; import { PrivacyPolicy } from "@policystack/react/policy"; import policy from "@/policy"; export function PrivacyPolicyPage() { return ( ); } ``` --- # Generate a privacy policy in React or export Markdown Source: https://policystack.dev/docs/policy/policies/quick-start > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). PolicyStack V1 renders privacy and cookie policies using your application's components, or exports Markdown, HTML, and PDF at build time. Start with the [complete shared configuration](https://policystack.dev/docs/quickstart#define-a-shared-configuration). ## Render a privacy policy in React ```sh pnpm add @policystack/sdk@1 @policystack/core@1 @policystack/react@1 ``` ```tsx import { PrivacyPolicy, CookiePolicy } from "@policystack/react/policy"; import policy from "./policystack"; export default function Policies() { return ( <> ); } ``` The imported config is the `policystack.ts` from the quickstart. Components are unstyled; customise the `components` prop or your stylesheet. V1 does not require a shadcn registry or supply a finished consent banner. ## Export Markdown, HTML, or PDF ```sh pnpm add @policystack/renderers@1 ``` ```ts import { writeFile } from "node:fs/promises"; import { compilePolicy } from "@policystack/renderers"; import policy from "./policystack"; const files = await compilePolicy(policy, "privacy", { formats: ["markdown", "html", "pdf"], }); for (const file of files) await writeFile(file.filename, file.content); ``` Run this in a TypeScript-capable build environment. It writes one file for each requested format. Review the generated documents before publication. [Emission rules and limitations](https://policystack.dev/docs/reference/support#which-documents-and-output-formats-are-supported). ## React Native / Expo `PrivacyPolicy` and `CookiePolicy` work in React Native (Expo) when you supply RN equivalents for every slot via the `components` prop. The wrapper element is also overridable via `Root` — without it, the component renders a `
` and Metro will throw `View config getter callback for component "div" must be a function`. ```tsx import { Linking, Pressable, Text, View } from "react-native"; import { PrivacyPolicy, type PolicyComponents } from "@policystack/react/policy"; import policy from "./policy"; const components: PolicyComponents = { Root: ({ children }) => {children}, Section: ({ children }) => {children}, Heading: ({ node }) => ( {node.value} ), Paragraph: ({ children }) => {children}, List: ({ children }) => {children}, ListItem: ({ children }) => ( {"\u2022 "} {children} ), Text: ({ node }) => <>{node.value}, Bold: ({ node }) => {node.value}, Italic: ({ node }) => {node.value}, Link: ({ node }) => ( Linking.openURL(node.href)}> {node.value} ), }; export function PrivacyScreen() { return ; } ``` The `style` prop accepts any value (typed `unknown`) so you can pass an RN `ViewStyle` straight through to your custom `Root`. Override the `Table*` slots too if your config produces tables. --- # Render privacy policies in React Source: https://policystack.dev/docs/policy/react > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). React adapter for Policy. Renders your `policystack.ts` config as React components — a privacy policy, a cookie policy, or individual sections — with every element overridable. ## Install ```sh bun add @policystack/react @policystack/sdk ``` Peer dependencies: `react >= 18`. ## Setup There is **one** provider. Wrap your app with `` from `@policystack/react/provider` and pass it your whole `policystack.ts` config — it supplies both the policy context (`` / ``) and the consent store. There is no separate config and no conversion step. ```tsx import { PolicyStack } from "@policystack/react/provider"; import policy from "@/policystack"; export default function RootLayout({ children }: { children: React.ReactNode }) { return {children}; } ``` You can also skip the provider and pass `config` directly to a component (handy for a one-off page or React Native). ## Components ### `` / `` Render the document for the current config. Props: - `config?` — a `PolicyStackConfig`. Omit it to read the config from the nearest `` provider. - `components?` — a `PolicyComponents` map of slot overrides (see below). - `style?` — passed through to the `Root` slot. ```tsx import { PolicyStack } from "@policystack/react/provider"; import { PrivacyPolicy, CookiePolicy } from "@policystack/react/policy"; import policy from "@/policystack"; export function PrivacyPolicyPage() { return ( ); } ``` The privacy policy is emitted when the config has a `data` block; the cookie policy when it has `cookies`. See [Privacy policy](https://policystack.dev/docs/policy/policies/privacy) and [Cookie policy](https://policystack.dev/docs/policy/policies/cookies) for the config side. ## Custom renderers Components render unstyled by default. Pass a `components` prop to supply your own renderer for any slot — headings, paragraphs, lists, links, tables. The `PolicyComponents` type is the canonical slot contract; every key is optional and falls back to the default renderer. ```tsx import { PrivacyPolicy, type PolicyComponents } from "@policystack/react/policy"; import policy from "@/policystack"; const components: PolicyComponents = { Root: ({ children }) =>
{children}
, Heading: ({ node }) => node.level && node.level >= 3 ? (

{node.value}

) : (

{node.value}

), Link: ({ node }) => ( {node.value} ), }; export function PrivacyScreen() { return ; } ``` The default renderers (`DefaultRoot`, `DefaultHeading`, …) and the low-level `renderDocument` helper are exported too, if you want to wrap rather than replace a slot. ## React Native / Expo `` and `` work in React Native when you supply RN equivalents for every slot via `components` — including `Root`, or Metro throws on the default `
`. See the worked example in the [Quick Start](https://policystack.dev/docs/policy/policies/quick-start#react-native--expo). ## See also - [Quick Start](https://policystack.dev/docs/policy/policies/quick-start) — add policy pages to your app - [Configuration](https://policystack.dev/docs/policy/configuration) — the `policystack.ts` reference - [`@policystack/vue/policy`](https://policystack.dev/docs/policy/vue) · [`@policystack/svelte/policy`](https://policystack.dev/docs/policy/svelte) — other adapters - [Consent docs](https://policystack.dev/docs/consent) — the same config also drives the cookie banner ## License Apache-2.0 --- # Examples Source: https://policystack.dev/docs/policy/references/examples > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Example projects live in the [GitHub repository](https://github.com/jamiedavenport/policystack/tree/main/examples). | Example | Stack | | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | [TanStack](https://github.com/jamiedavenport/policystack/tree/main/examples/tanstack) | TanStack Start + `@policystack/react` + `@policystack/sdk` | The PolicyStack site itself ([`apps/web`](https://github.com/jamiedavenport/policystack/tree/main/apps/web)) is also a working reference: it dogfoods `@policystack/{react,sdk}`, rendering [`/privacy`](https://policystack.dev/privacy) from a typed `policystack.ts` and gating analytics behind a consent banner derived from the same config. --- # Supported jurisdictions Source: https://policystack.dev/docs/policy/references/jurisdictions > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Policy uses lowercase-kebab region codes for the `jurisdictions` field in your `policystack.ts`. `JurisdictionId` includes seven top-level regions and all 50 US states. TypeScript accepts only these codes and the runtime validator rejects anything else; there is no second enum and no migration alias. There are no regulation-name aliases like `"gdpr"` or `"ccpa"` — use the region code the regulation applies to. The code for the EU/EEA is `"eea"`, not `"eu"`: GDPR applies EEA-wide. ## Codes Every supported region resolves to one of two tiers: - **`specific`** — hand-authored, jurisdiction-precise policy text and user rights. - **`equivalent`** — posture-correct (opt-in vs. opt-out) with parent-jurisdiction text, plus a suppressible `jurisdiction-generic-policy-text` validator warning so the honesty gap is visible. A legitimate, shippable tier — a member's tier may be upgraded post-1.0 without a breaking change. | Code | Region | Regulation(s) | Tier | | ------------ | ----------------------- | --------------------------- | ------------ | | `eea` | European Economic Area | GDPR | `specific` | | `uk` | United Kingdom | UK-GDPR + PECR | `specific` | | `us-ca` | California, USA | CCPA / CPRA | `specific` | | `ch` | Switzerland | revFADP | `equivalent` | | `br` | Brazil | LGPD | `equivalent` | | `ca` | Canada | PIPEDA (+ Quebec Law 25) | `equivalent` | | `us` | United States (federal) | Federal baseline, opt-out | `equivalent` | | `us-` | Any US state | State privacy law posture | `equivalent` | | `row` | Rest of world | Conservative opt-in default | `equivalent` | US privacy law is state-level. `"us"` is the federal opt-out baseline; use the lowercase ISO postal code for a state, such as `"us-ca"`, `"us-fl"`, or `"us-tx"`. All 50 state codes inherit text and posture from `"us"` as their parent. California is the only state currently upgraded to hand-authored `specific` policy text. ```text us-al us-ak us-az us-ar us-ca us-co us-ct us-de us-fl us-ga us-hi us-id us-il us-in us-ia us-ks us-ky us-la us-me us-md us-ma us-mi us-mn us-ms us-mo us-mt us-ne us-nv us-nh us-nj us-nm us-ny us-nc us-nd us-oh us-ok us-or us-pa us-ri us-sc us-sd us-tn us-tx us-ut us-vt us-va us-wa us-wv us-wi us-wy ``` For programmatic checks, core and the SDK export `US_STATE_JURISDICTION_IDS`, `USStateJurisdictionId`, and the `isUSStateJurisdictionId()` type guard. ## What each `specific` code adds ### `eea` — GDPR - **Legal basis** section (Article 13) - **GDPR supplemental disclosures** (data controller, transfer safeguards, complaint rights) - **User rights**: access, rectification, erasure, portability, restriction, objection - **Cookie policy**: European-user disclosure under ePrivacy + GDPR consent rules ### `uk` — UK-GDPR - **Legal basis** section (Article 13) - **UK-GDPR supplemental disclosures**: Information Commissioner's Office (ICO) named as the supervisory authority, link to the ICO complaint portal, Data Protection Act 2018 referenced as the implementing statute, UK international transfer safeguards - **User rights**: same six rights as GDPR - **Cookie policy**: UK-user disclosure under PECR + UK-GDPR consent rules ### `us-ca` — CCPA / CPRA - **California Privacy Rights** supplement (Right to Know, Right to Delete, Right to Opt-Out, Right to Non-Discrimination) - **User rights**: access, erasure, opt_out_sale, non_discrimination ## Combining codes When multiple codes apply, their content is combined — user rights are deduplicated and ordered canonically, and each jurisdiction-specific supplement renders once. For example: ```ts jurisdictions: ["eea", "uk", "us-ca"], ``` produces a policy with GDPR, UK-GDPR, and CCPA supplements, plus the union of all three rights sets. ## Validation The runtime validator rejects any code that isn't a member of the union with compact guidance: ``` Unknown jurisdiction "eu" — valid top-level codes: eea, uk, ch, br, ca, us, row; US states use us- (for example, us-ca or us-tx) ``` If you are upgrading from a pre-1.0 release, the common migration is `"eu"` → `"eea"`. The codes `"au"`, `"jp"`, and `"sg"` are not canonical jurisdictions and are rejected — declare `"row"` for a conservative opt-in fallback if you serve those regions. --- # Render privacy policies in Svelte Source: https://policystack.dev/docs/policy/svelte > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). Svelte 5 adapter for Policy. Renders your `policystack.ts` config as Svelte components, with every element overridable via snippets. ## Install ```sh bun add @policystack/svelte @policystack/sdk ``` Peer dependencies: `svelte >= 5` (the policy renderer is runes-based). ## Setup There is **one** provider, and for Svelte it ships from the same entry as the components: `@policystack/svelte/policy`. Wrap your app with `` and pass it your whole `policystack.ts` config — it supplies the policy context (`` / ``) and the consent store. (Consent hooks themselves import from `@policystack/svelte/consent`.) ```svelte {@render children()} ``` You can also skip the provider and pass `config` directly to a component. ## Components ### `` / `` Render the document for the current config. Props: - `config?` — a `PolicyStackConfig`. Omit it to read the config from the nearest ``. - `style?` — a CSS string, passed through to the `Root` slot. - One optional **snippet** per slot (`Root`, `Heading`, `Link`, …) — see below. ```svelte ``` The privacy policy is emitted when the config has a `data` block; the cookie policy when it has `cookies`. See [Privacy policy](https://policystack.dev/docs/policy/policies/privacy) and [Cookie policy](https://policystack.dev/docs/policy/policies/cookies) for the config side. ## Snippet overrides Unlike React/Vue (which take a `components` object), the Svelte adapter takes one snippet **prop per slot**. The `PolicyComponents` type maps each canonical slot to a `Snippet`; every slot receives its `node`, and container slots (`Root`, `Section`, `List`, table slots) additionally receive a `children` snippet. Anything you don't override falls back to the default renderer. ```svelte {#snippet Heading({ node })} = 3 ? "h3" : "h2"} class="font-medium"> {node.value} {/snippet} {#snippet Link({ node })} {node.value} {/snippet} ``` To wrap rather than replace the document shell, override `Root` and render its `children` snippet inside your own markup. The `Default*` slot components are exported too. ## See also - [Quick Start](https://policystack.dev/docs/policy/policies/quick-start) — add policy pages to your app - [Configuration](https://policystack.dev/docs/policy/configuration) — the `policystack.ts` reference - [`@policystack/react/policy`](https://policystack.dev/docs/policy/react) · [`@policystack/vue/policy`](https://policystack.dev/docs/policy/vue) — other adapters - [Consent docs](https://policystack.dev/docs/consent) — the same config also drives the cookie banner ## License Apache-2.0 --- # Render privacy policies in Vue Source: https://policystack.dev/docs/policy/vue > **PolicyStack V1** — current documentation. [Supported capabilities and limitations](https://policystack.dev/docs/reference/support). > **Vue 1.5.0 packaging limitation:** the published package omits `@policystack/vue/provider`. Provider examples below describe the repository implementation and require a release exporting `./provider`. Direct policy rendering with `config` remains available. [Support details](https://policystack.dev/docs/reference/support). Vue 3 adapter for Policy. Renders your `policystack.ts` config as Vue components, with every element overridable. The API mirrors the [React adapter](https://policystack.dev/docs/policy/react), translated to Vue idioms. ## Install ```sh bun add @policystack/vue @policystack/sdk ``` Peer dependencies: `vue >= 3`. ## Setup There is **one** provider. Wrap your app with `` from `@policystack/vue/provider` and pass it your whole `policystack.ts` config — it supplies both the policy context (`` / ``) and the consent store. No separate config, no conversion step. ```vue ``` You can also skip the provider and pass `:config` directly to a component. ## Components ### `` / `` Render the document for the current config. Props: - `config?` — a `PolicyStackConfig`. Omit it to read the config from the nearest `` provider. - `components?` — a `PolicyComponents` map of slot overrides (see below). - `style?` — a Vue `CSSProperties` object, passed through to the `Root` slot. ```vue ``` The privacy policy is emitted when the config has a `data` block; the cookie policy when it has `cookies`. See [Privacy policy](https://policystack.dev/docs/policy/policies/privacy) and [Cookie policy](https://policystack.dev/docs/policy/policies/cookies) for the config side. ## Custom renderers Components render unstyled by default. Pass a `components` prop to supply your own renderer for any slot. The `PolicyComponents` type is the canonical slot contract — keys are optional and fall back to the default renderer. ```vue ``` The default renderers (`DefaultRoot`, `DefaultHeading`, …) and the low-level `renderDocument` helper are exported too, if you want to wrap rather than replace a slot. ## See also - [Quick Start](https://policystack.dev/docs/policy/policies/quick-start) — add policy pages to your app - [Configuration](https://policystack.dev/docs/policy/configuration) — the `policystack.ts` reference - [`@policystack/react/policy`](https://policystack.dev/docs/policy/react) · [`@policystack/svelte/policy`](https://policystack.dev/docs/policy/svelte) — other adapters - [Consent docs](https://policystack.dev/docs/consent) — the same config also drives the cookie banner ## License Apache-2.0 --- # Add privacy policies and cookie consent to React Source: https://policystack.dev/docs/quickstart This PolicyStack V1 quickstart adds a privacy policy and a minimal consent choice UI to an existing React 18+ TypeScript application. PolicyStack supplies the state and renderers; you own the banner design and accessibility. ## Install the packages ```sh pnpm add @policystack/sdk@1 @policystack/core@1 @policystack/react@1 ``` Alternatively, `pnpm dlx @policystack/cli@1 init` scaffolds a starter config and an agent reference. The manual example below is complete and uses fictional company details: replace them with your reviewed disclosures. ## Define a shared configuration Create `policystack.ts`: ```ts import { ContractPrerequisite, defineConfig, LegalBases } from "@policystack/sdk"; export default defineConfig({ company: { name: "Acme", legalName: "Acme Ltd", address: "1 High Street, London", url: "https://acme.example", contact: { email: "privacy@acme.example" }, }, effectiveDate: "2026-09-06", jurisdictions: ["eea", "uk"], data: { collected: { Account: ["Email address"] }, context: { Account: { purpose: "Create and operate an account", lawfulBasis: LegalBases.Contract, retention: "Until account deletion", provision: ContractPrerequisite("An account cannot be created without it."), }, }, }, cookies: { used: { essential: true, analytics: true }, context: { essential: { lawfulBasis: LegalBases.LegalObligation }, analytics: { lawfulBasis: LegalBases.Consent }, }, }, }); ``` Company values are explicit: V1 does not populate them from `package.json`. Use [configuration guidance](https://policystack.dev/docs/policy/configuration) and validation to review jurisdiction-dependent disclosures before publishing. ## Render the policy and consent controls Create or replace `App.tsx`: ```tsx import { PolicyStack } from "@policystack/react/provider"; import { PrivacyPolicy } from "@policystack/react/policy"; import { ConsentGate, useConsent } from "@policystack/react/consent"; import policy from "./policystack"; function ConsentControls() { const { route, acceptAll, acceptNecessary, setRoute } = useConsent(); return (
{route === "cookie" ? ( <>

Allow optional analytics or use necessary cookies only.

) : ( )}
); } export default function App() { return ( Analytics is off.

}>

Analytics consent has been granted.

); } ``` ## Verify the behaviour With no saved choice, the optional analytics gate is closed. Accept all opens it; necessary only keeps it closed. Change the choice to reject after accepting and the gate closes again. The privacy document renders from the same configuration. The example displays a consent state, not a tracking script. To load a real vendor, use [GatedScript and the script factories](https://policystack.dev/docs/consent/scripts). Closing a gate does not unload an already executed vendor script: implement the vendor's opt-out/reset behaviour where needed. See [storage and SSR](https://policystack.dev/docs/consent/core) when persisting choices or rendering on the server. ## Next steps - [Render a cookie policy or export Markdown](https://policystack.dev/docs/policy/policies/quick-start). - [Build a preferences panel with useCategory](https://policystack.dev/docs/consent/react). - [Detect ungated analytics in Vite](https://policystack.dev/docs/consent/vite). - [Validate your configuration and connect MCP](https://policystack.dev/docs/policy/cli). Implementation: [React consent bindings](https://github.com/jamiedavenport/policystack/tree/main/packages/react/src), [core consent store](https://github.com/jamiedavenport/policystack/tree/main/packages/core/src/consent). --- # V1 support matrix and limitations Source: https://policystack.dev/docs/reference/support PolicyStack V1 is a TypeScript-first library for policy generation and headless consent. This matrix describes the 1.5.0 release line and checked-in implementation, reviewed on 6 September 2026. Packaging differences are called out explicitly. ## Which frameworks does PolicyStack support? | Integration | Policy rendering | Consent bindings | GatedScript | | ---------------------- | ---------------- | ------------------------- | ------------------------- | | React 18+ | Yes | Yes | Yes | | Vue 3.5+ | Yes | Yes, see packaging caveat | Yes, see packaging caveat | | Svelte 5 | Yes | Yes, runes and stores | Yes | | Solid 1.8+ | No | Yes | Yes | | Angular 20+ | No | Yes | No | | Framework-neutral core | Document AST | Consent store | gateScript / gateScripts | The Vue 1.5.0 published export map omits `@policystack/vue/provider`, although this repository implements it. Direct policy rendering with a `config` prop works; shared-provider examples require a release whose export map includes `./provider`. Do not assume repository source is already published. Solid 1.5.0 exports TypeScript source, so the consuming toolchain must transpile dependency source. Svelte's store API is available, but the package requires Svelte 5; it is not a Svelte 4 compatibility guarantee. ## Which documents and output formats are supported? V1 generates **privacy and cookie policies**. The core compiles a renderer-neutral document AST. `@policystack/renderers` exports Markdown, HTML, and PDF; React, Vue, and Svelte provide policy components. Terms of service, DPAs, DPIAs, and data-subject request workflows are not included. A `data` or `children` declaration triggers privacy emission; `cookies` triggers cookie emission. `trackingTechnologies` alone does not auto-emit a cookie policy. An explicit `policies` selection controls emission. The privacy compiler currently requires non-empty collected data even though the validator permits an empty declaration with a warning. ## Does PolicyStack include a cookie banner? No finished banner or preferences UI ships with V1. PolicyStack is headless: the application supplies layout, copy, accessibility, and vendor integration. The [React quickstart](https://policystack.dev/docs/quickstart) demonstrates a minimal UI. ## Does PolicyStack automatically block every cookie? No. Vite analysis detects some ungated source usage. Runtime enforcement happens only where applications call the consent APIs or use gates. Static scanning is heuristic, cannot prove all data flows are covered, and skips unsupported dynamic declarations. Revoking consent does not undo earlier vendor actions or unload scripts that already ran. Seven vendor factories ship: GA4, Google Tag Manager, Meta Pixel, PostHog, Segment, Hotjar, and Microsoft Clarity. ## Which CLI and agent tools ship? `policystack init`, `policystack validate --json`, and `policystack mcp` are implemented. Consent-specific scan and sync shell commands are not implemented. Use the Vite scanner or MCP `scan_ungated` tool instead. The [agent guide](https://policystack.dev/docs/policy/agent-skills) links the generated SDK reference and four generated workflow skills. ## What jurisdiction and language coverage exists? Jurisdiction-specific policy text exists for `eea`, `uk`, and `us-ca`. Other supported jurisdiction IDs use generic text and emit `jurisdiction-generic-policy-text`; an accepted ID does not imply complete legal coverage. Consent posture and policy-text coverage are separate. Unknown countries fall back to conservative opt-in behaviour. Built-in boilerplate supports English, French, German, Dutch, and Spanish. User-supplied purposes, names, and retention statements are not translated. [Jurisdiction reference](https://policystack.dev/docs/policy/references/jurisdictions). ## Does V1 enforce consent in backend services? V1 provides cookie/header helpers and a generic HTTP storage adapter. It does not provide server middleware, identity-to-consent mapping, distributed revocation, Python/Go SDKs, or background-job enforcement. Applications must supply those integrations. ## What is planned for V2? A self-hostable control plane, privacy inventory, backend enforcement, and rights workflows are [planned V2 direction](https://policystack.dev/docs/roadmap), not shipped V1 capabilities. No availability date or price is committed here. ## Licensing and review Current V1 packages are Apache-2.0. Proposed V2 licensing is described separately on the roadmap and is subject to review. Generated documents and consent UX require human review; PolicyStack cannot determine your obligations or guarantee compliance. ## Implementation evidence - [Package manifests and source](https://github.com/jamiedavenport/policystack/tree/main/packages). - [Emission rules](https://github.com/jamiedavenport/policystack/blob/main/packages/core/src/emit.ts) and [compiler tests](https://github.com/jamiedavenport/policystack/tree/main/packages/core/src). - [CLI commands](https://github.com/jamiedavenport/policystack/blob/main/packages/cli/src/index.ts) and [MCP tools](https://github.com/jamiedavenport/policystack/blob/main/packages/cli/src/mcp/tools.ts).