Agent skill
policyengine-app
Developing policyengine-app-v2 — the main React frontend for policyengine.org
Install this agent skill to your Project
npx add-skill https://github.com/PolicyEngine/policyengine-claude/tree/main/skills/tools-and-apis/policyengine-app-skill
SKILL.md
PolicyEngine app (v2)
Architecture and patterns for developing the main PolicyEngine web application at policyengine.org.
Repository: PolicyEngine/policyengine-app-v2
Architecture
Monorepo
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
Tech stack
| 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 |
Dual SPA mode
VITE_APP_MODE controls which entry point builds:
website— Full policyengine.org (pages, blog, research, embedded tools)calculator— Standalone calculator at app.policyengine.org
Development
bun 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
CRITICAL: Frontend verification and troubleshooting
How to verify the frontend works
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.
Correct verification sequence
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
Dependency troubleshooting
When bun install fails:
- Read the error. Fix the specific issue.
- Try at most 2 approaches before asking the user for help.
- Hard stops — never do these:
rm -rf node_moduleswithout user approval- Manually
npm pack+tar -xzfpackages into node_modules - Editing lockfile internals
- Looping through increasingly obscure npm/bun flags
Design tokens
Import 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.
Mantine components
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 |
Charts
Recharts (standard for all new charts)
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>
);
}
Semantic chart colors
| Meaning | Token |
|---|---|
| Primary data | colors.primary[500] |
| Secondary | colors.gray[400] |
| Positive/gains | colors.success |
| Negative/losses | colors.gray[600] |
| Error | colors.error |
Plotly (maps only)
Plotly is only used for geographic visualizations (choropleths, hex maps). All other charts use Recharts.
Chart components
| Component | Purpose |
|---|---|
ChartContainer |
Card wrapper with title, CSV download |
ChartWatermark |
PolicyEngine logo below chart |
ImpactTooltip |
Formatted hover tooltip |
ImpactBarLabel |
Values above/below bars |
Routing
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)
Country guards
CountryGuardSimple— Validates countryId, redirects to defaultCountryAppGuard— Validates slug+countryId for apps
Adding a new page
- Create
app/src/pages/MyPage.page.tsx - Add route in
WebsiteRouter.tsxunder appropriate layout - Use
useParams()forcountryId
Embedded apps (apps.json)
Interactive 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.
Ingredient CRUD pages
Policies, Reports, Simulations, and Populations follow a shared pattern using IngredientReadView and RenameIngredientModal. See app/.claude/skills/ingredient-patterns.md for details.
Blog posts
Markdown files in app/src/data/posts/articles/. Metadata in posts.json.
Sentence case
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).
Deployment
- Automatic on push to
mainvia Vercel - Domain:
policyengine.org(website),app.policyengine.org(calculator) - Team:
policy-enginescope
Key files
| 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) |
URL patterns
Production domains
| Domain | Purpose |
|---|---|
policyengine.org |
Marketing website, research, blog |
app.policyengine.org |
Calculator app (policies, households, reports) |
Calculator URLs (app.policyengine.org)
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
Website URLs (policyengine.org)
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)
Legacy v1 URLs (DO NOT USE)
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.
Related skills
policyengine-design-skill— Full token referencepolicyengine-interactive-tools-skill— Building standalone toolspolicyengine-vercel-deployment-skill— Deployment patternspolicyengine-writing-skill— Content stylepolicyengine-api-skill— Backend API
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
policyengine-healthcare
Healthcare program modeling in PolicyEngine-US — Medicaid, ACA marketplace, CHIP, and Medicare. Covers encoding rules, running analyses, and navigating the unique complexity of US healthcare programs. Triggers: "healthcare", "health insurance", "Medicaid", "ACA", "CHIP", "Medicare", "marketplace", "premium tax credit", "APTC", "PTC", "SLCSP", "benchmark plan", "rating area", "age curve", "family tier", "coverage gap", "Medicaid expansion", "MAGI", "medicaid_magi", "aca_magi", "medicaid_income_level", "medicaid_category", "enrollment", "takeup", "take-up", "per capita", "CSR", "cost sharing", "insurance premium", "second lowest silver", "required contribution percentage", "42 CFR", "IRC 36B", "categorical eligibility", "expansion adult", "healthcare reform", "healthcare analysis", "health policy".
policyengine-us
ALWAYS LOAD THIS SKILL FIRST before writing any PolicyEngine-US code. Contains the correct API patterns for household calculations and population simulations using the new policyengine package. Covers US federal and state taxes/benefits. Triggers: "what would", "how much would a", "benefit be", "eligible for", "qualify for", "single parent", "married couple", "family of", "household of", "if they earn", "earning $", "making $", "calculate benefits", "calculate taxes", "benefit for a", "what would I get", "what is the maximum", "what is the rate", "poverty line", "income limit", "benefit amount", "maximum benefit", "compare states", "TANF", "SNAP", "EITC", "CTC", "SSI", "WIC", "Section 8", "Medicaid", "ACA", "child tax credit", "earned income", "supplemental security", "housing voucher", "microsimulation", "population", "reform", "policy impact", "budgetary", "decile".
policyengine-uk
ALWAYS LOAD THIS SKILL FIRST before writing any PolicyEngine-UK code. Contains the correct API patterns for household calculations and population simulations using the new policyengine package (not policyengine_uk directly). Triggers: "what would", "how much would a", "benefit be", "eligible for", "qualify for", "single parent", "married couple", "family of", "household of", "if they earn", "with income of", "earning £", "making £", "calculate benefits", "calculate taxes", "benefit for a", "tax for a", "what would I get", "what would they get", "what is the rate", "what is the threshold", "personal allowance", "maximum benefit", "income limit", "benefit amount", "how much is", "Universal Credit", "child benefit", "pension credit", "housing benefit", "council tax", "income tax", "national insurance", "JSA", "ESA", "PIP", "disability living allowance", "working tax credit", "child tax credit", "Scotland", "Wales", "UK", "microsimulation", "population", "reform", "policy impact", "budgetary", "decile".
policyengine-canada
ALWAYS LOAD THIS SKILL FIRST before writing any PolicyEngine-Canada code. Contains Canadian federal and provincial tax/benefit rules for household calculations. IMPORTANT: PolicyEngine-Canada does NOT have representative population microdata. Do NOT attempt microsimulation or population-level estimates for Canada. Only provide household-level analysis (single-family impacts, eligibility, benefit amounts). Triggers: "what would", "how much would a", "benefit be", "eligible for", "qualify for", "single parent", "married couple", "family of", "household of", "if they earn", "earning $", "making $", "calculate benefits", "calculate taxes", "benefit for a", "what would I get", "what is the maximum", "what is the rate", "income limit", "benefit amount", "maximum benefit", "compare provinces", "CCB", "Canada Child Benefit", "GST credit", "HST credit", "GST/HST", "OAS", "Old Age Security", "GIS", "Guaranteed Income Supplement", "CWB", "Canada Workers Benefit", "EI", "Employment Insurance", "CPP", "Canada Pension Plan", "RRSP", "TFSA", "Ontario Child Benefit", "OCB", "Ontario Trillium Benefit", "OTB", "BC Climate Action", "Alberta Child Benefit", "Quebec", "CRA", "Canada Revenue Agency", "Canadian", "Canada", "Ontario", "British Columbia", "Alberta", "Saskatchewan", "Manitoba", "Nova Scotia", "New Brunswick", "PEI", "Newfoundland", "Yukon", "NWT", "Nunavut", "provincial tax", "federal tax Canada".
policyengine-ui-kit-consumer
This skill should be used when setting up a new project that uses @policyengine/ui-kit, debugging CSS or styling issues in a consumer app, or when Tailwind utility classes are not being generated. Also use when creating globals.css, configuring PostCSS, or troubleshooting "no styles", "no spacing", or "no layout" problems. Triggers: "ui-kit import", "globals.css setup", "Tailwind not working", "styles not applying", "utility classes missing", "setup ui-kit", "PostCSS config", "no styling", "CSS broken", "import ui-kit", "theme.css", "no layout", "no spacing", "@tailwindcss/postcss"
policyengine-tailwind-shadcn
Tailwind CSS v4 + shadcn/ui integration patterns for PolicyEngine frontend projects. Covers @theme namespaces, CSS variable conventions, SVG var() usage, and common mistakes. Triggers: "Tailwind v4", "@theme", "shadcn", "CSS variables", "design tokens CSS", "theme.css", "@theme inline"
Didn't find tool you were looking for?