22 KiB
Reactive Resume: agent instructions
This file applies across the repository. Follow a closer AGENTS.md when one exists. Keep this guide focused on agent workflows; user-facing documentation lives in README.md and docs/. Format guidance: agents.md.
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Rules:
- Drop: articles (a/an/the), filler (just/really/basically), pleasantries, hedging
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
- Pattern: [thing] [action] [reason]. [next step].
- Not: "Sure! I'd be happy to help you with that."
- Yes: "Bug in auth middleware. Fix:"
Switch level: /caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra Stop: "stop caveman" or "normal mode"
Auto-Clarity: drop caveman for security warnings, irreversible actions, user confused. Resume after.
Boundaries: code/commits/PRs written normal.
This is NOT the Turborepo you know
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the turbo package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run node -p "require.resolve('turbo/package.json')" from a workspace that depends on turbo.
Read docs/README.md inside that installed package first, then read the relevant pages from its docs/ directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
This block is written and re-added by turbo before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in crates/turborepo-cli/src/cli/agent_guidance.rs. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set "agentGuidance": false in the root turbo.json or turbo.jsonc to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
Agent skills
- Issues and specs: GitHub Issues for
reactive-resume/reactive-resume. Seedocs/agents/issue-tracker.md. - Check
git status --shortbefore editing. Preserve unrelated changes, including existing edits in this file. - Use scripts and configuration as the source of truth when documentation disagrees with them.
Overview
Reactive Resume is a free, open-source resume builder for creating, importing, exporting, and sharing resumes, cover letters, and job applications. It is a TypeScript pnpm monorepo managed by Turborepo, with two apps: apps/web (React 19 SPA with TanStack Router, TanStack Query, Tailwind CSS, and Vite) and apps/server (Hono / Node.js). oRPC connects browser workflows to server business logic; Better Auth handles authentication; Drizzle accesses PostgreSQL. Forme renders PDFs in the browser and on the server.
The production Docker image runs a single Node.js process on port 3000; apps/server mounts the API/auth/MCP/static routes and serves the built web app. On Vercel, the frontend service serves static assets through its CDN and the backend service runs the same Hono application in a Node.js Function.
Internal packages are source-consumed through package.json export maps pointing at src files. Do not assume package-local dist output exists unless a package explicitly adds it.
Setup
Prerequisites: Node.js 24 (.nvmrc, root engines, and Dockerfile), pnpm 12.8.1 (root packageManager; pnpm self-manages to this version), and Docker with Docker Compose for local infrastructure. The Dockerfile's ARG PNPM_VERSION chooses its base image, not the project's pnpm version. Start your Docker daemon before running Compose.
Shared dependency versions live in the default catalog in pnpm-workspace.yaml. Use catalog: in workspace manifests when that shared range applies; keep intentional exact pins and peer dependency ranges explicit.
Run commands from the workspace root unless stated otherwise:
pnpm install --frozen-lockfile
test -e .env.local || cp .env.example .env.local
docker compose -f compose.dev.yml up -d postgres redis seaweedfs seaweedfs_create_bucket
docker compose -f compose.dev.yml ps
Copy the environment template only when .env.local does not already exist. For host-run development, edit these values in .env.local; the template uses container hostnames:
APP_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
S3_ENDPOINT=http://localhost:8333
REDIS_URL=redis://localhost:6379
Set AUTH_SECRET to a generated secret (openssl rand -hex 32). If using saved AI providers or the assistant, also set a separate ENCRYPTION_SECRET of at least 32 characters. For database-only development, start just postgres and set STORAGE_BACKEND=local to avoid the template's S3 defaults.
Development workflow
pnpm dev
pnpm dev:web
pnpm db:generate
pnpm db:migrate
pnpm db:studio
pnpm devruns Vite onPORT(default3000), Hono onSERVER_PORT(default3001), and the email template preview on3002. Vite proxies API requests to Hono. Vite supplies hot reload;tsx watchrestarts the server.pnpm dev:webstarts only Vite; API workflows still need a server. If ports are busy, changePORTandSERVER_PORTconsistently in.env.local; keep the email preview's3002port free when running all dev tasks.- Server startup applies migrations before initializing auth and serving traffic.
pnpm db:migrateapplies them without starting the app;pnpm db:studioopens the database UI. - After adding user-facing strings, use Lingui macros and run
pnpm lingui:extract. Catalogs live inapps/web/locales/*.po;pnpm pdf:translationsregenerates PDF translations. Root build/check scripts run PDF translation generation automatically. pnpm docs:genregenerates the OpenAPI spec and semantic CSS reference. Use it when changing those public surfaces.
Ownership map
Where each concern lives, and where new code for it goes:
| Area | Owner |
|---|---|
| Web routes, loaders, user-facing workflows | apps/web/src/routes, apps/web/src/features (file-based; never hand-edit routeTree.gen.ts) |
| Server HTTP routes/adapters, startup checks, static handlers, MCP transport, OpenAPI/well-known | apps/server/src/{http,rpc,mcp,openapi,static,startup} |
| Authenticated API contracts + business logic | packages/api/src/features/* (oRPC routers, DTOs, rate limiting; aggregated at @reactive-resume/api/routers for /api/rpc) |
| Auth | packages/auth (Better Auth config/helpers/types; apps/server/src/http/auth.ts delegates to auth.handler) |
| DB client + schema | packages/db (Drizzle; migrations at repo root migrations/) |
| Server env validation | packages/env (auto-loads root .env) |
| Resume/page/template Zod schemas | packages/schema |
| Pure resume-domain behavior (no DB/HTTP/DOM/renderer deps) | packages/resume (JSON Patch helpers, social-network icons) |
| Resume PDF rendering | packages/pdf (React templates converted through src/forme to Forme documents, font resolution, browser/server adapters) |
| PDF.js viewer/canvas UI | apps/web/src/features/resume — never in packages/pdf |
| DOCX export | packages/docx |
| MCP tools/prompts/resources/server-card | packages/mcp |
| Generic UI primitives + hooks | packages/ui (Base UI/shadcn-style); workflow-specific UI stays in the owning web feature |
| DeepSeek Harness integration | packages/dsh-plugin (separately built/published plugin) |
| Focused support surfaces | packages/fonts, packages/email, packages/import, packages/ai, packages/utils, packages/config — prefer existing exports over cross-package shortcuts |
| Dev-only scripts | tooling/, not packages/, so packages only hold runtime-bundled code |
Narrow cross-cutting helpers go in packages/utils only after checking no domain package is a better owner. Specifically: resume JSON Patch behavior belongs in @reactive-resume/resume/patch and DOCX builders in @reactive-resume/docx — not in @reactive-resume/utils.
Web app conventions
apps/web/src/router.tsxinitializes router context withqueryClient,orpc,theme,locale,session, andflags. Reuse route context instead of refetching these ad hoc.- The web app is a client-rendered SPA. The web build prerenders marketing homepages per locale; there is no request-time React SSR.
apps/server/src/static/web.tsserves HTML and injects OpenGraph, canonical, and JSON-LD metadata. When adding a public marketing route, update its server fallback/SEO handling as well as the TanStack route; Vite's dev fallback can otherwise hide production 404s. - Builder shell:
apps/web/src/routes/builder/$resumeId. Public resume route:apps/web/src/routes/$username/$slug.tsx. - Browser-only preview code:
apps/web/src/features/resume/preview. Public PDF viewer:apps/web/src/features/resume/public. Keep PDF.js/canvas code in these features, not inpackages/pdf. - oRPC client:
apps/web/src/libs/orpc/client.tscalls/api/rpcwith credentials included.apps/web/src/libs/orpc/fetch.tsstages large request bodies through Blob on Vercel. - For React components with explicit props, use a named props type (e.g.
type FooProps = {...}withfunction Foo(props: FooProps)) rather than inline object annotations, especially with more than one field or with generics.
Package boundaries
pnpm exec turbo boundaries is the executable check. Rules:
- Workspace deps go through package names and export maps. Never import another workspace's
srctree via repo paths,@reactive-resume/*/src/*, or TS path aliases. - Workspace
turbo.jsonfiles declare coarse tags:app:web,app:server,runtime:server(server-only packages: API/auth/db/env/email/MCP),runtime:browser(browser-only shared UI),runtime:universal(environment-neutral domain packages), plusrole:domain|infra|adapter|api|rendering|toolingfor intent. - Runtime-specific code lives behind explicit export subpaths (
@reactive-resume/pdf/browser,@reactive-resume/pdf/server,@reactive-resume/env/server). Keep root exports environment-neutral unless the package is intentionally server-only. - Wildcard exports are allowed only for leaf libraries with an intentionally file-like surface — currently
@reactive-resume/ui/components/*,@reactive-resume/ui/hooks/*, and schema resume/application model files. Prefer explicit exports for packages owning runtime behavior. - Prefer
protectedProcedurefrompackages/api/src/context.tsfor authenticated procedures. Expose only intentional public surfaces throughpackages/api/package.json. - Shared PDF section filtering:
packages/pdf/src/templates/shared/filtering.ts. Template-specific visual exceptions stay in the owning template directory unless multiple templates need the behavior.packages/pdf/src/hooks/use-register-fonts.tsresolves font families, weights, and script fallback stacks; the Forme adapter owns conversion/rendering. PDF generation needs no Browserless or Chromium service.
Multi-place changes:
- Resume data shape:
packages/schema/src/resume/*first, then API DTOs, importers, PDF rendering, and web forms consuming it. - New template:
packages/schema/src/templates.ts,packages/pdf/src/templates/index.ts, source underpackages/pdf/src/templates/<name>/, and previews underapps/web/public/templates/{jpg,pdf}. - New DB column/table:
packages/db/src/schema/*, thenpnpm db:generate. - New env var:
packages/env/src/server.ts,.env.example, and theglobalPassThroughEnvarray and applicable test-taskenvarrays inturbo.json. Add deployment aliases inpackages/env/src/deployment.tswhen needed. Turborepo strict env mode filters unlisted injected variables from task processes.
Environment and database
Host development requires APP_URL, DATABASE_URL, and non-empty AUTH_SECRET. packages/env/src/server.ts also loads root .env through Node's native process.loadEnvFile; existing process variables take precedence. Root dev/database scripts explicitly load .env.local through dotenvx. Tests and application code can have their own environment loaders; do not assume every command loads .env.local.
- Storage: explicit
STORAGE_BACKEND=local|s3|blobwins. Otherwise, complete S3 credentials select S3; Vercel selects private Blob; other deployments select local storage..env.exampleships SeaweedFS defaults, so either run SeaweedFS or selectlocal/remove the S3 credentials. Local storage defaults to<workspace>/datain development and/app/datain Docker.LOCAL_STORAGE_PATHmust be absolute and writable; persist it in deployed installations. ENCRYPTION_SECRETis required for saved AI providers and the assistant.REDIS_URLis optional outside Vercel; it shares rate limits, cancellation and resumable replies between processes. Vercel deployment preparation requires Redis. Host-run dev usesREDIS_URL=redis://localhost:6379; the container-run app usesredis://redis:6379.drizzle-kit(used bypnpm db:migrate) readsDATABASE_URLfromprocess.envdirectly — it does not auto-load.env. The root migration scripts load.env.localthroughdotenvxbefore invoking Drizzle Kit.DATABASE_MIGRATION_URLsupplies a direct migration connection when runtimeDATABASE_URLis pooled. Review generated migration SQL before applying it; avoid resetting databases or deleting volumes to fix setup errors.- Startup verifies the migrated schema.
STRICT_SCHEMA_CHECK=truemakes detected drift fatal; otherwise the server logs it and continues.
Testing and checks
Prefer package-scoped checks for the files changed. Package names come from their package.json: the apps are web and server, most shared packages are @reactive-resume/<name>.
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm --filter @reactive-resume/pdf test src/templates/shared/filtering.test.ts
pnpm --filter @reactive-resume/pdf exec vitest run src/templates/shared/filtering.test.ts -t "filterItems"
pnpm --filter @reactive-resume/pdf test:coverage
pnpm exec biome check apps/web/src/features/resume
pnpm exec turbo boundaries
- Vitest tests live alongside source as
src/**/*.test.ts(x)orsrc/**/*.spec.ts(x)(including integration tests). Paths underpnpm --filter <package>are package-relative. Pass paths directly aftertest: an extra--currently prevents Vitest from filtering the run. Shared settings live invitest.shared.mtsand setup invitest.setup.ts; most packages use Node, whilepackages/uiuseshappy-dom. - Coverage uses V8 and writes package-local
coverage/reports. No shared minimum coverage threshold is configured.test:ciwrites JSON/JUnit results under package-localreports/. - Root
pnpm test,pnpm test:coverage, andpnpm typecheckrun workspace checks through Turbo. CI checks boundaries and affected-package typechecks, then runs all unit suites withpnpm exec turbo run test:ci --concurrency=1to avoid CPU contention in PDF/rate-limit suites. Unit/browser and Vercel workflows persist.turbo/cache; cached coverage and test reports restore to package-local output directories. - Real-database unit suites use
COVER_LETTER_TEST_DATABASE_URLandOAUTH_TEST_DATABASE_URL; see.github/workflows/e2e.ymlfor isolated database setup. Never point test fixtures at production data. - After changing shared contracts, exports, or imports, check affected consumers and run
pnpm exec turbo boundaries.
Browser tests
Playwright specs live in tests/e2e/specs/*.spec.ts, with fixtures in tests/e2e/fixtures. Configure a disposable PostgreSQL database and export test environment variables before building/running; these root scripts do not wrap dotenvx.
pnpm exec playwright install chromium
pnpm build
pnpm test:e2e
pnpm test:e2e tests/e2e/specs/auth.spec.ts
pnpm test:e2e:ui
playwright.config.tsstartsnode apps/server/dist/index.mjsin production mode and waits for/api/health; locally it can reuse an existing server. Build first. Keep the direct Node command: pnpm's script process groups can prevent Playwright from cleaning up a server started throughpnpm start.- Export
APP_URL,PORT,DATABASE_URL,AUTH_SECRET, andENCRYPTION_SECRET, and choose an absolute writableLOCAL_STORAGE_PATH. Auth fixtures need signups/email auth enabled;FLAG_DISABLE_API_RATE_LIMIT=trueis appropriate for this isolated test installation. - Assistant specs use a deterministic local AI stub and need
FLAG_ALLOW_UNSAFE_AI_BASE_URL=true; otherwise those specs skip. Seetests/e2e/README.mdfor the full environment recipe; adapt its example storage path to your machine. - Playwright runs Chromium with no retries. CI uses one worker and retains failure traces, screenshots, videos, and reports. PDF/DOCX rasterization and visual regression are outside this browser gate.
Code style
- TypeScript is strict, including
exactOptionalPropertyTypes,noUncheckedIndexedAccess, and unused-symbol checks; packages typecheck withtsgo --noEmit. - Biome uses tabs, double quotes, 120-column lines, separated type imports, organized import groups, and sorted Tailwind classes in
clsx,cva, andcn. Use existing file naming and feature-local conventions. pnpm checkmodifies files: it regenerates PDF translations and runs Biome with--write --unsafe. Call out its write behavior and review the diff; use narrow non-mutating commands when inspecting unrelated edits.- Lefthook's pre-commit hook checks conflict markers and runs write-capable Biome on supported staged files, staging fixes. The commit-message hook enforces Conventional Commits (
fix:,feat:,docs:, etc.).
Build and deployment
pnpm build
NODE_ENV=production pnpm start
docker compose up -d --build
- Build outputs:
apps/web/dist(SPA/assets),apps/web/dist-prerender(localized marketing HTML), andapps/server/dist(index.mjsplus server/deployment chunks).pnpm startruns the built server; setNODE_ENV=productionso it usesPORTinstead ofSERVER_PORT. Export runtime variables or provide root.env;.env.localis not loaded bystart. - Production Compose loads
.env.examplethen.env, not.env.local. Configure.envwith container hostnames (postgres,redis,seaweedfs) and production secrets before running it. The Docker image runs asnode, listens on3000, and persists local storage through/app/data. Health endpoint:/api/health. vercel.jsondefines Vercel Services (project framework must beServices):frontend(apps/web, staticdist) andbackend(apps/server, entrypointapps/server/vercel.mjsre-exporting the tsdown build). The backend build runspnpm buildfor both apps, thennode apps/server/dist/prepare-deployment.mjs. Top-level rewrites send paths whose last segment has a file extension tofrontendand everything else, including HTML shells, tobackend, except the server-owned paths listed first. The Function uses Node 24 and a 300-second budget.outputDirectory: "."onbackendstops the builder from treatingdist/index.mjs(the Docker entrypoint) as the handler.- Vercel environment normalization accepts
POSTGRES_URL, direct/unpooled DB aliases, andKV_URL.APP_URLcan be derived from Vercel host variables. Blob is the default when no S3 credentials are set. Preview deployments require isolated resources before enablingALLOW_PREVIEW_MIGRATIONS=true; seedocs/self-hosting/vercel.mdx. .github/workflows/e2e.ymlgates core unit/browser flows;vercel.ymlbuilds and checks the serverless artifact on PRs and pushes tomain.autofix.ymlruns write-capablepnpm knip --fixandpnpm check. GitHub runners are the default;USE_BLACKSMITH=trueswitches runners and paired actions.docker-build.ymlpublishes native AMD64/ARM64 images.mainpublishes nightly aliases; release tags/explicit release dispatch publish stable aliases and can trigger configured production integrations. Seedocs/agents/container-publishing.mdbefore release work.- Deployment smoke tests create/delete accounts and files; run only against a dedicated test installation. Details:
docs/contributing/deployment-checks.mdx.
Security and pull requests
- Keep credentials and personal resume data out of source, logs, test artifacts, issues, and PRs. Do not commit local environment files or substitute production secrets for test values.
- Authenticated procedures use
protectedProcedure; enforce resource ownership in feature logic. Reuse shared auth resolution for API keys, bearer tokens, and cookies rather than adding a separate auth path. - Keep unsafe OAuth redirect/AI URL flags disabled on public deployments. They relax redirect validation and SSRF protections for trusted self-hosted/test use.
- Keep PRs focused. Describe the problem, resulting behavior, and checks actually run; link the relevant GitHub issue. Conventional Commits are enforced for commit messages; no separate PR-title convention is configured.
- Before submitting, run applicable typechecks/tests and non-mutating lint checks; run the production build for runtime/bundling changes. Match CI's database/browser prerequisites when reproducing its checks. Report skipped checks and failures instead of claiming they passed.
- Never add AI attribution, co-author trailers naming AI tools, or session/chat links to commits or PR descriptions.
Gotchas
- Email sending needs SMTP config; without it emails are logged to console. Dev still works — verification links appear in server logs.
- Database connection errors: check
docker compose -f compose.dev.yml psand uselocalhostfor host-run code, service names inside containers. - S3 errors: check
docker compose -f compose.dev.yml logs seaweedfs seaweedfs_create_bucket; verify endpoint and bucket, or select local storage. - Route-tree errors after adding routes: run Vite dev/build to regenerate
apps/web/src/routeTree.gen.ts; never edit it by hand. - Serverless module-loading failures: inspect
bundledInteropPackagesinapps/server/tsdown.config.tsand the Vercel compatibility workflow. External CommonJS server dependencies break on Vercel because its service builder drops their pnpm links; bundle them with their dependencies. - Most test scripts use
--passWithNoTests; a successful run with zero tests does not verify the behavior you changed.