$npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-appLoad when developing policyengine-app-v2 — the site served at policyengine.org. Covers the bun/turbo monorepo, the Next.js 15 App Router host in website/, the legacy Vite SPA in app/ being migrated, ui-kit + designTokens theming, multizone rewrites, and how the frontend calls the
| 1 | # PolicyEngine app v2 |
| 2 | |
| 3 | `PolicyEngine/policyengine-app-v2` is the frontend served at **policyengine.org**. It is |
| 4 | mid-migration: a **Next.js 15 App Router host lives in `website/`** and is the live apex, |
| 5 | while a **legacy Vite SPA in `app/`** still owns the calculator surface and is being ported |
| 6 | page by page. Verify the current stack from `package.json` files before trusting any summary |
| 7 | — this repo moves fast. |
| 8 | |
| 9 | ## Monorepo layout |
| 10 | |
| 11 | Bun workspaces + Turbo (`packageManager: bun@1.2.21`, root `turbo.json`). Workspaces: |
| 12 | `packages/*`, `app`, `website`, `calculator-app`. |
| 13 | |
| 14 | | Path | What | Bundler | |
| 15 | |---|---|---| |
| 16 | | `website/` | `@policyengine/website` — Next.js 15 App Router **host** (live apex, marketing/research/tools) | Next 15 + Turbopack | |
| 17 | | `app/` | `policyengine-app-v2` — legacy Vite SPA (calculator: policies, households, reports, simulations) | Vite 6 | |
| 18 | | `calculator-app/` | standalone calculator workspace | Vite | |
| 19 | | `packages/` | shared workspace packages | — | |
| 20 | |
| 21 | Root scripts run through Turbo: `bun run build` → `turbo run build`; `bun run dev` runs the |
| 22 | Next host (`scripts/dev-server-next.mjs`); `bun run dev:legacy` runs the Vite app. |
| 23 | |
| 24 | ## Tech stack (verified from package.json) |
| 25 | |
| 26 | There is **no third-party component framework** — UI is built from radix-ui primitives plus |
| 27 | local components. Do not add one. |
| 28 | |
| 29 | - `website/`: `next` ^15.3.3, `radix-ui` ^1.4.3, `@tabler/icons-react`, `tailwindcss` v4 |
| 30 | (`@tailwindcss/postcss`), `class-variance-authority`, `react` 19, `react-plotly.js` (maps), |
| 31 | `framer-motion`, `fuse.js`, `react-markdown`. |
| 32 | - `app/`: `vite` 6, `react-router-dom` 7, `@tanstack/react-query`, `@reduxjs/toolkit` + |
| 33 | `react-redux`, `recharts` ^3.7.0, `@tabler/icons-react`, `tailwindcss` v4 |
| 34 | (`@tailwindcss/vite`), Storybook. |
| 35 | - Both pin **`@policyengine/ui-kit` ^0.4.0**. See the policyengine-design skill for its API. |
| 36 | |
| 37 | `useDisclosure` / `useMediaQuery` / `useViewportSize` in `app/src/hooks/` are local |
| 38 | reimplementations — not a dependency. |
| 39 | |
| 40 | ## App Router page pattern (website/) |
| 41 | |
| 42 | Pages live under `website/src/app/[countryId]/<slug>/`. The convention is a **server |
| 43 | `page.tsx`** (exports `metadata`, awaits `params`) that renders a **`*Client.tsx`** client |
| 44 | component: |
| 45 | |
| 46 | ```tsx |
| 47 | // website/src/app/[countryId]/ai-agents/page.tsx (server) |
| 48 | import type { Metadata } from "next"; |
| 49 | import ClaudePluginClient from "./ClaudePluginClient"; |
| 50 | export const metadata: Metadata = { title: "Claude plugin", description: "..." }; |
| 51 | export default async function Page({ params }: { params: Promise<{ countryId: string }> }) { |
| 52 | const { countryId } = await params; // params is a Promise in Next 15 |
| 53 | return <ClaudePluginClient countryId={countryId} />; |
| 54 | } |
| 55 | ``` |
| 56 | |
| 57 | Styling is a **hybrid**: Tailwind v4 utilities use a `tw:` prefix |
| 58 | (`className="tw:block tw:no-underline"`) and inline styles pull from JS design tokens |
| 59 | (`import { colors, spacing, typography } from "@/designTokens"`). `@/components/ui` exports |
| 60 | local `Text` / `Title` / etc. The JS tokens are a **local shim**: `app/src/designTokens/` |
| 61 | (`colors.ts`, `spacing.ts`, `typography.ts`) is a runtime object kept in sync with ui-kit's |
| 62 | `theme.css` during migration, and `website/src/designTokens` re-exports it. Never hardcode |
| 63 | hex — use the token. |
| 64 | |
| 65 | ## Multizone routing |
| 66 | |
| 67 | New cross-app rewrites go in **`website/next.config.ts`**, not the root `vercel.json` (which |
| 68 | is legacy: favicons, SPA catch-all, pre-multizone proxies). External Next tools are |
| 69 | registered in **`website/src/data/appZoneRoutes.ts`** (`appZoneRewrites`, spread into |
| 70 | `rewrites().beforeFiles` so zones beat the dynamic `[slug]` route). `afterFiles` proxies |
| 71 | Modal/Vercel/GitHub-Pages apps (tracker, slides, taxsim, model docs). A CI guard fails PRs |
| 72 | that add country-prefixed `*.vercel.app` rewrites to `vercel.json`. Full zone mechanics |
| 73 | (basePath, assetPrefix, self-rewrites) are in the policyengine-tools skill. |
| 74 | |
| 75 | ## Calling the API |
| 76 | |
| 77 | The legacy calculator calls the **v1 API** (`app/src/constants.ts`: |
| 78 | `BASE_URL = 'https://api.policyengine.org'`). Verified shapes: |
| 79 | |
| 80 | - `GET {BASE_URL}/{country}/household/{id}` and `.../household/{id}/policy/{policyId}` — household calc |
| 81 | - `POST {BASE_URL}/{country}/household` — create household |
| 82 | - `GET {BASE_URL}/{countr |