Skip to content
PolicyStack
V1
Esc
navigateopen⌘Jpreview
On this page

Auto-collect

Automatically populate data.collected and thirdParties from your source code

PolicyStack V1 — current documentation. Supported capabilities and limitations.

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

bun add -D @policystack/vite

Setup

Add policyStack() to your Vite plugin array. The scan runs during buildStart and refreshes on change in dev.

// 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.

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

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.

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.

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

// 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

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

Last updated on September 6, 2026