skills/tools-and-apis/policyengine-app-skill/SKILL.md
Developing policyengine-app-v2 — the main React frontend for policyengine.org
npx skillsauth add policyengine/policyengine-claude policyengine-appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Architecture and patterns for developing the main PolicyEngine web application at policyengine.org.
Repository: PolicyEngine/policyengine-app-v2
policyengine-app-v2/
├── packages/
│ └── design-system/ # @policyengine/design-system (npm)
├── app/ # Main Vite application
│ ├── src/
│ │ ├── pages/ # Page components (*.page.tsx)
│ │ ├── components/ # Shared UI (charts, layouts, modals)
│ │ ├── routing/ # Guards, router config
│ │ ├── hooks/ # Custom React hooks
│ │ ├── designTokens/ # Re-exports from design-system
│ │ ├── styles/ # Mantine theme, global PostCSS
│ │ ├── data/ # Static data (apps.json, posts/)
│ │ ├── adapters/ # API fetch wrappers
│ │ ├── api/ # React Query hooks
│ │ ├── contexts/ # React Context providers
│ │ ├── types/ # TypeScript interfaces
│ │ └── utils/ # Formatters, helpers
│ ├── public/ # Static assets (logos, post images)
│ └── vite.config.mjs
├── turbo.json
└── package.json # Bun workspaces root
| Layer | Technology |
|-------|-----------|
| Package manager | Bun (primary), npm fallback |
| Build | Vite + Turbo |
| UI framework | Mantine v8 |
| Routing | React Router v7 (createBrowserRouter) |
| Charts | Recharts (standard), Plotly (maps only) |
| Server state | React Query |
| Design tokens | @policyengine/design-system |
| Language | TypeScript |
| Formatting | Prettier + ESLint |
| Testing | Vitest |
VITE_APP_MODE controls which entry point builds:
website — Full policyengine.org (pages, blog, research, embedded tools)calculator — Standalone calculator at app.policyengine.orgbun install # Install dependencies
bun run dev # Dev server (builds design-system first)
cd app && bun run prettier -- --write . # Format before committing
bun run lint # Lint (CI uses --max-warnings 0)
bun run build # Production build
bun run test # Tests
The ONLY reliable way to verify the frontend compiles correctly is bun run build. This catches import errors, TypeScript errors, and missing dependencies.
curl is NOT verification. Vite serves an HTML shell regardless of whether React components actually render. A 200 from curl means nothing for an SPA. Never use curl output as evidence that the frontend works.
You cannot visually verify a frontend. After the build passes and dev server starts, tell the user it's ready for them to check in the browser. Do not claim it "looks good."
Before making any claim about dev server status, check with lsof -i :5173. Do not assume it is running.
bun run build # Catches all compile-time errors
# If build passes and dev server needed:
cd app && VITE_APP_MODE=website ./node_modules/.bin/vite --port 5173 &
open http://localhost:5173/us # User checks visually
When bun install fails:
rm -rf node_modules without user approvalnpm pack + tar -xzf packages into node_modulesImport from @/designTokens (convenience layer that re-exports from the design-system package):
import { colors, spacing, typography } from '@/designTokens';
Never hardcode values:
// Wrong
style={{ color: '#319795', marginBottom: '16px' }}
// Correct
style={{ color: colors.primary[500], marginBottom: spacing.lg }}
See policyengine-design-skill for the full token reference.
All UI uses Mantine v8. Key components:
import { Stack, Group, Text, Button, Paper } from '@mantine/core';
import { colors, spacing } from '@/designTokens';
function PolicyCard({ title, description, onEdit }) {
return (
<Paper p={spacing.lg} withBorder>
<Stack gap={spacing.sm}>
<Text fw={600}>{title}</Text>
<Text c={colors.text.secondary} fz="sm">{description}</Text>
<Button variant="outline" onClick={onEdit}>Edit</Button>
</Stack>
</Paper>
);
}
| Component | Usage |
|-----------|-------|
| Stack, Group, Box | Layout |
| Text, Title | Typography |
| Button, ActionIcon | Controls |
| Paper, Card | Containers |
| Table, Modal, Tooltip | Complex UI |
| TextInput, Select, NumberInput | Forms |
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { ChartContainer, ChartWatermark, ImpactTooltip } from '@/components/charts';
import { colors } from '@/designTokens';
function RevenueChart({ data }) {
return (
<ChartContainer title="Revenue impact" csvData={data}>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data}>
<XAxis dataKey="name" />
<YAxis tickFormatter={(v) => v.toLocaleString('en-US', {
style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1,
})} />
<Tooltip content={<ImpactTooltip />} />
<Bar dataKey="value" fill={colors.primary[500]} />
</BarChart>
</ResponsiveContainer>
<ChartWatermark />
</ChartContainer>
);
}
| Meaning | Token |
|---------|-------|
| Primary data | colors.primary[500] |
| Secondary | colors.gray[400] |
| Positive/gains | colors.success |
| Negative/losses | colors.gray[600] |
| Error | colors.error |
Plotly is only used for geographic visualizations (choropleths, hex maps). All other charts use Recharts.
| Component | Purpose |
|-----------|---------|
| ChartContainer | Card wrapper with title, CSV download |
| ChartWatermark | PolicyEngine logo below chart |
| ImpactTooltip | Formatted hover tooltip |
| ImpactBarLabel | Values above/below bars |
Routes in app/src/WebsiteRouter.tsx:
/:countryId/
├── (StaticLayout)
│ ├── home (index)
│ ├── research/:slug (blog posts)
│ ├── brand/*, team, donate
│ └── model
├── (AppLayout)
│ └── :slug → AppPage.tsx (apps.json)
└── (full-page embeds)
CountryGuardSimple — Validates countryId, redirects to defaultCountryAppGuard — Validates slug+countryId for appsapp/src/pages/MyPage.page.tsxWebsiteRouter.tsx under appropriate layoutuseParams() for countryIdInteractive tools register in app/src/data/apps/apps.json and render via AppPage.tsx:
{
"type": "iframe",
"slug": "marriage",
"title": "Marriage calculator",
"source": "https://marriage-zeta-beryl.vercel.app/",
"countryId": "us",
"displayWithResearch": true
}
See policyengine-interactive-tools-skill for the full embedding pattern.
Policies, Reports, Simulations, and Populations follow a shared pattern using IngredientReadView and RenameIngredientModal. See app/.claude/skills/ingredient-patterns.md for details.
Markdown files in app/src/data/posts/articles/. Metadata in posts.json.
Strictly enforced everywhere:
// Correct
<Title order={2}>Your saved policies</Title>
// Wrong
<Title order={2}>Your Saved Policies</Title>
Exceptions: proper nouns (PolicyEngine), acronyms (IRS), official names (Child Tax Credit).
main via Vercelpolicyengine.org (website), app.policyengine.org (calculator)policy-engine scope| File | Purpose |
|------|---------|
| app/src/WebsiteRouter.tsx | Main routes |
| app/src/pages/AppPage.tsx | Renders embedded apps |
| app/src/data/apps/apps.json | Tool registry |
| app/src/designTokens/ | Token imports |
| app/src/components/charts/ | Chart components |
| packages/design-system/ | Token source of truth |
| app/.claude/skills/ | Local skills (design-tokens, chart-standards, ingredient-patterns) |
| Domain | Purpose |
|--------|---------|
| policyengine.org | Marketing website, research, blog |
| app.policyengine.org | Calculator app (policies, households, reports) |
app.policyengine.org/:countryId/ # Dashboard
app.policyengine.org/:countryId/policies # Saved policies
app.policyengine.org/:countryId/policies/create # Policy builder
app.policyengine.org/:countryId/households # Saved households
app.policyengine.org/:countryId/households/create # Household builder
app.policyengine.org/:countryId/reports # Saved reports
app.policyengine.org/:countryId/reports/create # Report builder
app.policyengine.org/:countryId/simulations # Saved simulations
app.policyengine.org/:countryId/simulations/create # Simulation builder
app.policyengine.org/:countryId/report-output/:reportId # Report output (overview)
app.policyengine.org/:countryId/report-output/:reportId/:subpage/:view # Specific chart
Report output subpages and views:
/report-output/:reportId/budget # Budget overview
/report-output/:reportId/distributional/incomeDecile # Distributional by income
/report-output/:reportId/distributional/wealthDecile # Distributional by wealth
/report-output/:reportId/winners-losers/incomeDecile # Winners/losers by income
/report-output/:reportId/winners-losers/wealthDecile # Winners/losers by wealth
/report-output/:reportId/poverty/age # Poverty by age
/report-output/:reportId/poverty/gender # Poverty by gender
/report-output/:reportId/poverty/race # Poverty by race (US only)
/report-output/:reportId/deep-poverty/age # Deep poverty by age
/report-output/:reportId/deep-poverty/gender # Deep poverty by gender
/report-output/:reportId/inequality # Inequality measures
Country IDs: us, uk, ca, ng, il
policyengine.org/:countryId/ # Country home
policyengine.org/:countryId/research # Research index
policyengine.org/:countryId/research/:slug # Research article
policyengine.org/:countryId/blog # Blog index
policyengine.org/:countryId/blog/:postName # Blog post
policyengine.org/:countryId/model # Policy model explorer
policyengine.org/:countryId/:slug # Embedded app (from apps.json)
The old policyengine-app (v1) used a different URL pattern that no longer works:
# WRONG — v1 format, returns "App not found"
policyengine.org/us/policy?reform=73278&baseline=2®ion=enhanced_us&timePeriod=2025
policyengine.org/us/reform/2/280039/over/2/us?focus=policyOutput.winnersAndLosers.incomeDecile
Always use app.policyengine.org for calculator functionality.
policyengine-design-skill — Full token referencepolicyengine-interactive-tools-skill — Building standalone toolspolicyengine-vercel-deployment-skill — Deployment patternspolicyengine-writing-skill — Content stylepolicyengine-api-skill — Backend APItools
ONLY use this skill when users explicitly ask about the PolicyEngine Python package installation, REST API endpoints, API authentication, rate limits, or policyengine.py client library. DO NOT use for household benefit/tax calculations — ALWAYS use policyengine-us or policyengine-uk instead. This skill is about the API/client tooling itself, not about calculating benefits or taxes.
development
ALWAYS USE THIS SKILL for PolicyEngine microsimulation, population-level analysis, winners/losers calculations. Triggers: microsimulation, share who would lose/gain, policy impact, national average, weighted analysis, cost, revenue impact, budgetary, estimate the cost, federal revenues, tax revenue, budget score, how much would it cost, how much would the policy cost, total cost of, aggregate impact, cost to the government, revenue loss, fiscal impact, poverty impact, child poverty, deep poverty, poverty rate, poverty reduction, how many people lifted out of poverty, SPM poverty, distributional impact, state tax, state-level, California, New York, UBI, universal basic income, flat tax, standard deduction, winners and losers, winners, losers, inequality, Gini, decile, SALT, marginal tax rate, effective tax rate. NOT for single-household calculations like "what would my benefit be" - use policyengine-us or policyengine-uk for those. Use this skill's code pattern; explore codebase for parameter paths if needed.
development
PolicyEngine API v2 - Next-generation microservices architecture with monorepo structure
development
ALWAYS LOAD THIS SKILL before setting up any Python environment or installing packages. Defines the standard: uv, Python 3.13, uv pip install, .venv at project root. Triggers: "set up python", "install python", "create a venv", "virtual environment", "pip install", "install packages", "uv pip", "uv venv", "python version", "VIRTUAL_ENV", "venv conflict", "which python", "activate", "deactivate", "run the script", "run with uv", "uv run", "pyproject.toml", "install dependencies", "install requirements", "install the package", "editable install", "pip install -e", "latest package", "latest version", "current version", "newest version".