Files
2026-05-27 23:31:58 +02:00

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). Use nvm install 24 && nvm use 24 if 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/web owns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.
  • apps/server owns the production Hono app, route composition, auth/RPC/MCP/OpenAPI handlers, static uploads, schema JSON, web-dist fallback serving, and startup checks.
  • packages/api contains oRPC routers, DTOs, rate limiting, and feature-owned API modules under packages/api/src/features/*. The router export at @reactive-resume/api/routers aggregates those feature routers for /api/rpc.
  • packages/auth contains Better Auth config, auth helper functions, and exported auth types. The server auth adapter in apps/server/src/http/auth.ts delegates to auth.handler.
  • packages/db contains the Drizzle client and schema. Migration files live at the repo root in migrations/.
  • packages/env defines server environment validation and auto-loads the root .env for app/server code.
  • packages/schema contains Zod schemas and typed resume/page/template models.
  • packages/pdf contains the React PDF document, font registration, shared template primitives, template implementations, and browser/server PDF generation adapters. PDF.js viewer UI stays in apps/web.
  • packages/resume contains pure resume-domain behavior such as JSON Patch helpers and social-network icon mapping.
  • packages/docx contains DOCX export generation.
  • packages/mcp contains MCP tools, prompts, resources, server-card generation, and tool metadata.
  • packages/ui contains shared Base UI/shadcn-style components and hooks.
  • packages/fonts, packages/email, packages/import, packages/ai, packages/utils, and packages/config provide focused support surfaces. Prefer their existing exports over adding cross-package shortcuts.
  • Development-only scripts live in tooling/, not under packages/, 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-edit apps/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 in apps/server, not in web routes.
  • apps/web/src/router.tsx initializes router context with queryClient, orpc, theme, locale, session, and flags. 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 route apps/web/src/routes/$username/$slug.tsx uses ssr: "data-only".
  • Browser-only resume preview code lives under apps/web/src/features/resume/preview, and public resume PDF viewer code lives under apps/web/src/features/resume/public. Keep PDF.js/canvas/browser APIs out of SSR paths and out of packages/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/rpc with 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 src tree through repository paths, @reactive-resume/*/src/*, or TypeScript path aliases.
  • turbo boundaries is the executable package-boundary check. Workspace-level turbo.json files declare coarse tags:
    • app:web for the TanStack Start app.
    • app:server and runtime:server for the Node/Hono process.
    • runtime:server for server-only packages such as API/auth/db/env/email/MCP.
    • runtime:browser for browser-only shared UI.
    • runtime:universal for environment-neutral domain packages.
    • role:domain, role:infra, role:adapter, role:api, role:rendering, and role:tooling for 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 through packages/api/package.json. Prefer protectedProcedure from packages/api/src/context.ts for authenticated procedures.
  • Add database columns/tables in packages/db/src/schema/*, then generate root-level migrations with dotenvx 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 under packages/pdf/src/templates/<name>/, and static previews under apps/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.ts owns React PDF font registration, standard PDF font handling, CJK fallback stacks, and global hyphenation behavior.
  • PDF generation helpers live behind @reactive-resume/pdf/browser and @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/utils has narrowly exported helpers. If another package needs a utility, add an explicit export path instead of importing private files.

Placement decision tree:

  1. If the change is a web route, route loader, or user-facing web workflow, start in apps/web/src/routes or apps/web/src/features.
  2. 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.
  3. If it is authenticated API behavior, put the contract and implementation in the owning packages/api/src/features/* module.
  4. If it is pure resume data behavior with no DB, HTTP, DOM, or PDF renderer dependency, put it in packages/resume.
  5. If it renders resume PDFs, put shared React PDF/template code in packages/pdf; put PDF.js viewer/canvas UI in apps/web/src/features/resume.
  6. If it creates DOCX exports, put it in packages/docx.
  7. If it exposes MCP tools/prompts/resources, put it in packages/mcp.
  8. 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.
  9. If it is a narrow cross-cutting helper, add an explicit packages/utils export 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 (default http://localhost:3000)
  • DATABASE_URL (default postgresql://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:migrate is 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.yml pre-commit hook runs biome check on staged files. Run pnpm check before committing to avoid hook failures.
  • pnpm check is 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, and cn.
  • Most packages use tsgo --noEmit for typechecking and vitest run --passWithNoTests for tests.
  • There may be unrelated local edits in the worktree. Inspect git status --short first and avoid reverting files you did not touch.
  • New env vars require a turbo.json entry. Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in globalEnv (or task-level env/passThroughEnv). Any new environment variable added to packages/env/src/server.ts must also be added to the globalEnv array in turbo.json, or the variable will be undefined inside child processes at runtime even if it is correctly set in the OS/container environment.