OPEN-SOURCE POLICY-AS-CODE

The First Policy Framework for Busy Founders and Developers

March 6, 2026

Ship a Privacy Policy and Terms of Service with Your TanStack App

Most TanStack Start apps need a privacy policy and terms of service before they launch. The usual approach: grab a template from the internet, paste it into a static page, and forget about it until a lawyer asks why it still says “Company Name Here.”

OpenPolicy treats your policies like code. You define them as TypeScript objects, and the Vite plugin compiles them to HTML at build time — in sync with every deploy.

Install

bun add -D @openpolicy/sdk @openpolicy/vite

Add the plugin to vite.config.ts

import { openPolicy } from "@openpolicy/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    openPolicy({
      formats: ["html"],
      outDir: "src/policies",
    }),
    tanstackStart(),
    viteReact(),
  ],
});

The openPolicy() plugin goes before tanstackStart(). On the first bun run dev, if the config file doesn’t exist yet, the plugin scaffolds it automatically. Edit the generated file and save — the plugin watches for changes and regenerates.

Define your policies

Create a single openpolicy.ts at the root of your project. The unified defineConfig() lets you define all policies in one file with a shared company block:

// openpolicy.ts
import { defineConfig } from "@openpolicy/sdk";

export default defineConfig({
  company: {
    name: "Acme",
    legalName: "Acme, Inc.",
    address: "123 Main St, San Francisco, CA 94105",
    contact: "privacy@acme.com",
  },
  privacy: {
    effectiveDate: "2026-03-06",
    dataCollected: {
      "Account information": ["Email address", "Display name"],
      "Usage data": ["Pages visited", "Session duration"],
    },
    legalBasis: "Legitimate interests and user consent",
    retention: {
      "Account data": "Until account deletion",
      "Analytics data": "13 months",
    },
    cookies: {
      essential: true,
      analytics: true,
      marketing: false,
    },
    thirdParties: [
      { name: "Vercel", purpose: "Hosting and edge delivery" },
      { name: "Plausible", purpose: "Privacy-friendly analytics" },
    ],
    userRights: ["access", "erasure", "portability", "objection"],
    jurisdictions: ["us", "eu"],
  },
  terms: {
    effectiveDate: "2026-03-06",
    acceptance: {
      methods: ["using the service", "creating an account"],
    },
    eligibility: {
      minimumAge: 13,
    },
    accounts: {
      registrationRequired: true,
      userResponsibleForCredentials: true,
      companyCanTerminate: true,
    },
    prohibitedUses: [
      "Violating any applicable laws or regulations",
      "Attempting to gain unauthorized access to any part of the service",
      "Transmitting malware or malicious code",
    ],
    intellectualProperty: {
      companyOwnsService: true,
      usersMayNotCopy: true,
    },
    disclaimers: {
      serviceProvidedAsIs: true,
      noWarranties: true,
    },
    limitationOfLiability: {
      excludesIndirectDamages: true,
      liabilityCap: "the amount paid by the user in the past 12 months",
    },
    governingLaw: {
      jurisdiction: "Delaware, USA",
    },
    changesPolicy: {
      noticeMethod: "email or prominent notice on the website",
      noticePeriodDays: 30,
    },
  },
});

What gets generated

After the next build (or on save in dev), the plugin writes:

src/policies/
  privacy-policy.html
  terms-of-service.html

Because the files land inside src/, Vite can resolve them as ?raw imports directly from your route components.

Render on dedicated routes

Create a route file for each policy. TanStack Router picks them up automatically via file-based routing:

// src/routes/privacy.tsx
import { createFileRoute } from "@tanstack/react-router";
import html from "../policies/privacy-policy.html?raw";

export const Route = createFileRoute("/privacy")({
  component: RouteComponent,
});

function RouteComponent() {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// src/routes/terms.tsx
import { createFileRoute } from "@tanstack/react-router";
import html from "../policies/terms-of-service.html?raw";

export const Route = createFileRoute("/terms")({
  component: RouteComponent,
});

function RouteComponent() {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

TanStack Router auto-generates routeTree.gen.ts when it detects the new files — no manual registration needed.

Add a .gitignore entry so the generated files aren’t checked in:

# .gitignore
src/policies/

Why this is better than a static page

  • Type-safe. Every field is checked by TypeScript. You can’t ship a policy with a missing contact email.
  • Structured. Each section is generated from your actual config — no stale placeholder text.
  • Version-controlled. The config lives in your repo. git blame shows you when and why anything changed.
  • Jurisdiction-aware. Set jurisdictions: ["eu"] and GDPR-required sections (right to erasure, data transfers, DPA contact) are included automatically.

The generated HTML includes all required sections for the jurisdictions you specify. You own the config; OpenPolicy handles the legal structure.