policyengine-app
Developing policyengine-app-v2 — the main React frontend for policyengine.org
What this skill does
# 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
```bash
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
```bash
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:
1. Read the error. Fix the specific issue.
2. Try at most **2 approaches** before asking the user for help.
3. **Hard stops — never do these:**
- `rm -rf node_modules` without user approval
- Manually `npm pack` + `tar -xzf` packages 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):
```tsx
import { colors, spacing, typography } from '@/designTokens';
```
**Never hardcode values:**
```tsx
// 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:
```tsx
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)
```tsx
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 default
- `CountryAppGuard` — Validates slug+countryId for apps
### Adding a new page
1. Create `app/src/pages/MyPage.page.tsx`
2. Add route in `WebsiteRouter.tsx` under appropriate layout
3. Use `useParams()` for `countryId`
## Embedded apps (apps.json)
Interactive tools register in `app/src/data/apps/apps.json` and render via `AppPage.tsx`:
```json
{
"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:
```tsx
// Correct
<Title order={2}>Your saved policies</Title>
// Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.