[00] OpenPolicy

Configuration

Setting up your openpolicy.ts config file

All policies are defined in a single config file using defineConfig() from @openpolicy/sdk. You can place it anywhere in your project.

Install#

The fastest way is to run @openpolicy/cli, which installs @openpolicy/sdk plus the right framework integration for your stack and scaffolds a starter config:

bunx @openpolicy/cli init

Or install manually:

bun add @openpolicy/sdk

Create your config#

// openpolicy.ts
import { ContractPrerequisite, defineConfig, LegalBases, Voluntary } from "@openpolicy/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: ["eu", "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 required and shared across all policy types. All other fields live at the top level: effectiveDate and jurisdictions are shared, and OpenPolicy auto-detects which policies to generate from the fields you provide — include the data block for a privacy policy, and the cookies block (or consentMechanism / trackingTechnologies) for a cookie policy.

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.

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.

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):

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:

company: {
  // ...
  dpo: { required: false, reason: "Our processing is not large-scale or systematic." },
},

Omitting dpo emits a validation warning when jurisdictions includes eu 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:

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 @openpolicy/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 openPolicy() Vite plugin re-validates it at build time (see Build-time validation). When auto-collect is enabled, the plugin also emits openpolicy.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 eu 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 for the full list of codes.

Build-time validation#

The openPolicy() Vite plugin loads your resolved openpolicy.ts and runs every validator in @openpolicy/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 [openpolicy] 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):

// vite.config.ts
openPolicy({ validate: false });

Using AI#

The fastest way to fill out your config is to hand it to a coding agent. @openpolicy/cli prints a ready-made prompt for this — run bunx @openpolicy/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.