Claude
Skills
Sign in
Back

policyengine-interactive-tools

Included with Lifetime
$97 forever

Building standalone interactive calculators and dashboards that embed in policyengine.org

General

What this skill does


# PolicyEngine interactive tools

How to build standalone React apps (calculators, dashboards, visualizations) that integrate into policyengine.org as Next.js multi-zones (preferred) or via iframe (legacy).

## Examples

Use `/new-tool` plus this skill as the canonical scaffold for new repos. The production repos below are useful pattern references, but they do **not** all match the current frontend standard.

### Pattern references

- GiveCalc (`PolicyEngine/givecalc`) — custom Modal API with policyengine-us
- State legislative tracker (`PolicyEngine/state-legislative-tracker`) — precomputed/static data with external app embedding
- SNAP BBCE repeal dashboard (`PolicyEngine/snap-bbce-repeal`) — precomputed CSV dashboard

### Legacy repos still in production

- Marriage calculator (`PolicyEngine/marriage`) — older Vite-era frontend; use for business logic, not scaffolding
- ACA reforms calculator (`PolicyEngine/ACA-Calc`) — older Vite-era frontend; use for data ideas, not scaffolding
- Student loan calculator (`PolicyEngine/student-loan-calculator`) — legacy design-system/CDN setup; do not copy
- Spring Statement dashboard (`PolicyEngine/uk-spring-statement-2026`) — custom migration case; use for policy logic, not baseline architecture

## Current default stack

**New tools default to Next.js 14 + Tailwind 4 + Recharts.** Some deployed tools predate this stack; treat those repos as migration targets or pattern references, not templates.

| Component | Choice |
|-----------|--------|
| Framework | Next.js 14 (App Router) |
| CSS | Tailwind 4 with `@policyengine/ui-kit` theme |
| Charts | Recharts |
| Code highlighting | Prism React Renderer |
| Testing | Vitest |
| Deploy | Vercel under `policy-engine` scope |
| Package manager | `bun` (not npm) |

**Requirements:**
- `@policyengine/ui-kit` theme (installed via `bun add @policyengine/ui-kit`)
- Inter font via Google Fonts CDN
- Recharts for charts
- **NEVER hardcode hex colors or font names** — always use CSS variables from the ui-kit theme (e.g., `var(--primary)`, `var(--chart-1)`, `var(--font-sans)`)
- **PolicyEngine logo** — always use the actual logo image, never styled text. Copy it locally from `@policyengine/ui-kit` or PolicyEngine assets; never hotlink a raw GitHub URL
- **Every new repo needs pull-request CI before launch** — at minimum install + build, and add lint/test when scripts exist
- Sentence case on all UI text
- **App-specific provenance belongs in the app's results/methodology footnote** — `policyengine.py` versions, model/data versions, static-estimate caveats, and calculation notes must not be added to the shared PolicyEngine header, footer, or global layout. When touching these notes, add or preserve a regression test that the shared chrome does not contain app-specific provenance and the results/methodology footnote does.

## Multi-zone integration (preferred)

PolicyEngine tools integrate with policyengine.org as **Next.js multi-zones**. The host website (`policyengine-app-v2/website/`) proxies specific URL paths to standalone Vercel deployments via `rewrites`, so users see one site while each tool remains independently deployable.

**Multi-zone replaces iframe embedding for all new tools.** Iframe embedding is retained only for legacy tools and the `obbba-iframe` / `custom` apps.json types — see "Legacy iframe embedding" below.

### The decision rule

Pick the pattern from the zone's path shape first, then layer on the build-type config:

| Zone owns | Pattern |
|---|---|
| One public path matching the repo's kebab name | **Path-mounted zone** — literal `basePath` |
| Multiple public paths (e.g. `/us/api` + `/uk/api`) | **Root-served zone** — no `basePath`, host rewrites map each public path to the zone root |

| Zone build type | Additional config |
|---|---|
| Server-rendered, path-mounted | None — `basePath` scopes `_next/*` automatically |
| Server-rendered, root-served | `assetPrefix: '/_zones/<repo-name>'` (zone has no basePath, so `_next/*` would collide with the host without a prefix) |
| Static export (`output: 'export'`), either pattern | Phase-gated `assetPrefix: '/_zones/<repo-name>'` + `vercel.json` self-rewrite |

### Two valid Next.js zone patterns

Both patterns are endorsed by the official docs — pick the one that fits the zone's path shape.

- **Path-mounted zone:** `basePath: '/us/my-tool'`. Hardcoded string matching the public path. Used by the official Next.js [`with-zones` example](https://github.com/vercel/next.js/tree/canary/examples/with-zones). **Default for single-path zones** — i.e. when the zone owns exactly one public path that matches the repo name in kebab case.
- **Root-served zone:** no `basePath`; host rewrites map each public path to the zone's root. Used by the [Next.js multi-zones guide's own example](https://nextjs.org/docs/app/guides/multi-zones) (the blog zone uses `assetPrefix: '/blog-static'` with no basePath). Requires `assetPrefix: '/_zones/<repo-name>'` so the zone's `_next/static/*` assets do not collide with the host or other zones. **Required when one zone owns multiple public paths**, since `basePath` accepts only one literal string. Production reference: `household-api-docs` (serves both `/us/api` and `/uk/api` from one deployment via `[countryId]` dynamic routes).

The Next.js multi-zones guide states only one cross-zone constraint: *"URL paths should be unique to a zone."* A zone can own multiple paths; two zones can't share one.

> **Why no env-driven `basePath` (e.g. `process.env.NEXT_PUBLIC_BASE_PATH ?? '/us/my-tool'`)?**
> Neither documented pattern uses an env override. The intended dev workflow is to hit the zone at `localhost:<port>/<basePath>` directly (path-mounted) or `localhost:<port>/<public-path>` (root-served), or to run the host with its `rewrites()` `destination` pointed at `localhost:<zone-port>` for end-to-end local development. There is no docs-endorsed "drop the basePath in dev" escape hatch, and adding one hides basePath bugs that would otherwise surface in dev. A few existing zones (`keep-your-pay-act`, `oregon-kicker-refund`, `working-parents-tax-relief-act`) still use this pattern; they're tracked for retrofit but are not multizone blockers.

Host rewrite shape depends on the chosen pattern — see the "Canonical zone config" sections below; each shows the matching host rewrite inline.

### What each config controls

- `basePath`: **route URLs** — page paths, `next/link` hrefs, API route paths. Auto-scopes `_next/static/` assets for server-rendered builds.
- `assetPrefix`: **static asset URLs** only — `_next/static/*`, `next/image`, `next/script`. Needed when `basePath` can't scope assets automatically (static exports) or when the zone has no `basePath` (root-served).

Both may coexist — they govern different URL types and never conflict.

### Canonical zone config — server-rendered (common case)

```ts
// zone's next.config.ts
const nextConfig: NextConfig = {
  basePath: '/us/my-tool',
  // no assetPrefix needed — basePath scopes _next/static automatically
};
export default nextConfig;
```

```ts
// host: policyengine-app-v2/website/next.config.ts — in beforeFiles
// Hardcode the zone's production Vercel URL (whatever Vercel auto-assigned on first deploy).
{ source: '/us/my-tool',        destination: 'https://my-tool.vercel.app/us/my-tool' },
{ source: '/us/my-tool/:path*', destination: 'https://my-tool.vercel.app/us/my-tool/:path*' },
```

### Canonical zone config — static export

Static exports need **three coordinated pieces** — each covers a different environment; omitting any one breaks that environment. (The root-served variant of this pattern, with no `basePath`, is in production at `PolicyEngine/household-api-docs`.)

**1. Zone's `next.config.mjs` — phase-gated `assetPrefix`**

```js
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js';

export default function nextConfig(phase) {
  const isDev = phase === PHASE_DEVELOPMENT_SERVER;

  return {
    output: 'export',
    basePath: '/

Related in General