13 KiB
AGENTS.md
Cursor Cloud specific instructions
Overview
Reactive Resume is a pnpm monorepo (Turborepo) with two deployable apps: apps/web (TanStack Start / React 19 / Vite) and apps/server (Hono / Node.js). The production Docker image runs a single Node.js process on port 3000, with apps/server mounting the API/auth/MCP/static routes and serving the built web app.
Internal packages are source-consumed through package.json export maps that point at src files. Do not assume package-local dist output exists unless a package explicitly adds it.
Prerequisites
- Node.js 24 (matches Dockerfile
ARG NODE_VERSION=24). Usenvm install 24 && nvm use 24if needed. - Docker is required to run PostgreSQL. Start it with
sudo dockerd &if the daemon isn't running. - pnpm 11.1.2 is managed via corepack (
corepack enable).
Codebase map
apps/webowns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.apps/serverowns the production Hono app, route composition, auth/RPC/MCP/OpenAPI handlers, static uploads, schema JSON, web-dist fallback serving, and startup checks.packages/apicontains oRPC routers, DTOs, rate limiting, and feature-owned API modules underpackages/api/src/features/*. The router export at@reactive-resume/api/routersaggregates those feature routers for/api/rpc.packages/authcontains Better Auth config, auth helper functions, and exported auth types. The server auth adapter inapps/server/src/http/auth.tsdelegates toauth.handler.packages/dbcontains the Drizzle client and schema. Migration files live at the repo root inmigrations/.packages/envdefines server environment validation and auto-loads the root.envfor app/server code.packages/schemacontains Zod schemas and typed resume/page/template models.packages/pdfcontains the React PDF document, font registration, shared template primitives, template implementations, and browser/server PDF generation adapters. PDF.js viewer UI stays inapps/web.packages/resumecontains pure resume-domain behavior such as JSON Patch helpers and social-network icon mapping.packages/docxcontains DOCX export generation.packages/mcpcontains MCP tools, prompts, resources, server-card generation, and tool metadata.packages/uicontains shared Base UI/shadcn-style components and hooks.packages/fonts,packages/email,packages/import,packages/ai,packages/utils, andpackages/configprovide focused support surfaces. Prefer their existing exports over adding cross-package shortcuts.- Development-only scripts live in
tooling/, not underpackages/, so packages only contain code bundled by the app/runtime.
Web app conventions
- Routes are file-based under
apps/web/src/routes. Do not hand-editapps/web/src/routeTree.gen.ts; it is generated by TanStack Router tooling. - Server-owned HTTP behavior lives in
apps/server/src/{http,rpc,mcp,openapi,static,startup}. Keep API/RPC/auth/MCP/static route wiring inapps/server, not in web routes. apps/web/src/router.tsxinitializes router context withqueryClient,orpc,theme,locale,session, andflags. Reuse route context where possible instead of refetching these concerns ad hoc.- The builder shell lives under
apps/web/src/routes/builder/$resumeId. The nested preview route is client-only (ssr: false), while the public resume routeapps/web/src/routes/$username/$slug.tsxusesssr: "data-only". - Browser-only resume preview code lives under
apps/web/src/features/resume/preview, and public resume PDF viewer code lives underapps/web/src/features/resume/public. Keep PDF.js/canvas/browser APIs out of SSR paths and out ofpackages/pdf. - The isomorphic oRPC client is in
apps/web/src/libs/orpc/client.ts; server calls use an in-process router client and browser calls use/api/rpcwith credentials included. - For React components with explicit props, prefer a named TypeScript props type over inline object annotations in the function signature, especially once the props include more than one field or generics. For example:
type IntentSelectFieldProps<TValue extends string> = {
label: string;
id: string;
value: TValue | undefined;
options: readonly ComboboxOption<TValue>[];
onChange: (value: TValue | undefined) => void;
};
function IntentSelectField<TValue extends string>(props: IntentSelectFieldProps<TValue>) {
// ...
}
Package and feature boundaries
- Workspace dependencies must go through package names and package export maps. Do not import another workspace's
srctree through repository paths,@reactive-resume/*/src/*, or TypeScript path aliases. turbo boundariesis the executable package-boundary check. Workspace-levelturbo.jsonfiles declare coarse tags:app:webfor the TanStack Start app.app:serverandruntime:serverfor the Node/Hono process.runtime:serverfor server-only packages such as API/auth/db/env/email/MCP.runtime:browserfor browser-only shared UI.runtime:universalfor environment-neutral domain packages.role:domain,role:infra,role:adapter,role:api,role:rendering, androle:toolingfor package intent.
- Browser/server runtime-specific code should live behind explicit export subpaths such as
@reactive-resume/pdf/browser,@reactive-resume/pdf/server, or@reactive-resume/env/server. Keep root exports environment-neutral unless the package is intentionally server-only. - Wildcard exports are allowed only for leaf libraries whose public surface is intentionally file-like, currently
@reactive-resume/ui/components/*,@reactive-resume/ui/hooks/*, and schema resume model files. Prefer explicit exports for packages that own runtime behavior. - Add new API procedures and business logic inside the owning
packages/api/src/features/*module. Keep route wiring, DTO usage, helpers, and services colocated by feature/capability, then expose only intentional public surfaces throughpackages/api/package.json. PreferprotectedProcedurefrompackages/api/src/context.tsfor authenticated procedures. - Add database columns/tables in
packages/db/src/schema/*, then generate root-level migrations withdotenvx run -f .env.local -- pnpm db:generate. - Add or change resume data shape in
packages/schema/src/resume/*first, then update API DTOs, importers, PDF rendering, and web forms that consume that shape. - Add or rename templates in all relevant places:
packages/schema/src/templates.ts,packages/pdf/src/templates/index.ts, template source underpackages/pdf/src/templates/<name>/, and static previews underapps/web/public/templates/{jpg,pdf}. - Resume JSON Patch behavior belongs in
@reactive-resume/resume/patch; do not put resume-domain helpers in@reactive-resume/utils. - DOCX export behavior belongs in
@reactive-resume/docx; do not put DOCX builders in@reactive-resume/utils. - Shared PDF section filtering lives in
packages/pdf/src/templates/shared/filtering.ts. Keep template-specific visual exceptions in the owning template directory unless multiple templates need the same behavior. packages/pdf/src/hooks/use-register-fonts.tsowns React PDF font registration, standard PDF font handling, CJK fallback stacks, and global hyphenation behavior.- PDF generation helpers live behind
@reactive-resume/pdf/browserand@reactive-resume/pdf/server; locale-specific section-title resolution stays in the caller. - MCP implementation belongs in
@reactive-resume/mcp; app packages must not import MCP implementation from another app's source tree. packages/utilshas narrowly exported helpers. If another package needs a utility, add an explicit export path instead of importing private files.
Placement decision tree:
- If the change is a web route, route loader, or user-facing web workflow, start in
apps/web/src/routesorapps/web/src/features. - If the change is a server HTTP route/adapter, startup check, static handler, MCP transport, or OpenAPI/well-known handler, start in
apps/server/src. - If it is authenticated API behavior, put the contract and implementation in the owning
packages/api/src/features/*module. - If it is pure resume data behavior with no DB, HTTP, DOM, or PDF renderer dependency, put it in
packages/resume. - If it renders resume PDFs, put shared React PDF/template code in
packages/pdf; put PDF.js viewer/canvas UI inapps/web/src/features/resume. - If it creates DOCX exports, put it in
packages/docx. - If it exposes MCP tools/prompts/resources, put it in
packages/mcp. - If it is a generic UI primitive or hook, put it in
packages/ui; if it is workflow-specific UI, keep it in the owning web feature. - If it is a narrow cross-cutting helper, add an explicit
packages/utilsexport only after checking that no domain package is a better owner.
Database
PostgreSQL runs via Docker Compose:
sudo docker compose -f compose.dev.yml up -d postgres
The dev default connection string is postgresql://postgres:postgres@localhost:5432/postgres.
Important: drizzle-kit (used by pnpm db:migrate) reads DATABASE_URL from process.env directly — it does not auto-load the .env file. Run migration commands through dotenvx, for example dotenvx run -f .env.local -- pnpm db:migrate, so DATABASE_URL is present in the process environment.
The production server runs migrations during startup before serving traffic. Manual pnpm db:migrate is mainly for first setup, migration debugging, or applying migrations without starting the app.
Environment
Copy .env.example to .env. The three required variables are:
APP_URL(defaulthttp://localhost:3000)DATABASE_URL(defaultpostgresql://postgres:postgres@localhost:5432/postgres)AUTH_SECRET(any non-empty string)
S3/SeaweedFS is optional. If S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_BUCKET are all set, the app uses S3-compatible storage. The checked-in .env.example sets SeaweedFS defaults, so either start the seaweedfs compose service too or comment out those S3 vars to use local filesystem storage under <workspace>/data. LOCAL_STORAGE_PATH must be absolute when set.
When running dev servers or migration commands, prefix the command with dotenvx run -f .env.local --. For example: dotenvx run -f .env.local -- pnpm dev. Tests, typechecks, linters, boundary checks, and pnpm build do not need this prefix by default. If one of those commands fails because a specific environment variable is required, rerun it with the dotenvx run -f .env.local -- prefix.
Common commands
| Task | Command |
|---|---|
| Install deps | pnpm install |
| Start Postgres only | sudo docker compose -f compose.dev.yml up -d postgres |
| Start Postgres + SeaweedFS | sudo docker compose -f compose.dev.yml up -d postgres seaweedfs seaweedfs_create_bucket |
| Generate migrations | dotenvx run -f .env.local -- pnpm db:generate |
| Run migrations | dotenvx run -f .env.local -- pnpm db:migrate |
| Dev server | dotenvx run -f .env.local -- pnpm dev (starts on port 3000) |
| Web dev server only | dotenvx run -f .env.local -- pnpm dev:web |
| Lint/format | pnpm check (Biome) |
| Boundary check | pnpm exec turbo boundaries |
| Tests | pnpm test (Vitest) |
| Build | pnpm build |
| Typecheck | pnpm typecheck |
For focused validation, prefer package filters before repo-wide commands, for example:
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm --filter @reactive-resume/api test
pnpm exec turbo boundaries
Vitest test paths are package-relative when running through pnpm --filter <package> test -- <path>.
Gotchas
- The server startup path auto-runs migrations before serving traffic, so
pnpm db:migrateis mainly needed for first-time setup, migration debugging, or applying migrations without starting the app. - Email sending requires SMTP config; without it, emails are logged to console. This is fine for dev — the app still functions, but email verification links appear in server logs.
- The
lefthook.ymlpre-commit hook runsbiome checkon staged files. Runpnpm checkbefore committing to avoid hook failures. pnpm checkis write-capable (biome check --write --unsafe .). Call that out when using it, and use narrower Biome commands if you need a non-mutating inspection.- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for
clsx,cva, andcn. - Most packages use
tsgo --noEmitfor typechecking andvitest run --passWithNoTestsfor tests. - There may be unrelated local edits in the worktree. Inspect
git status --shortfirst and avoid reverting files you did not touch. - New env vars require a
turbo.jsonentry. Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed inglobalEnv(or task-levelenv/passThroughEnv). Any new environment variable added topackages/env/src/server.tsmust also be added to theglobalEnvarray inturbo.json, or the variable will beundefinedinside child processes at runtime even if it is correctly set in the OS/container environment.