Skip to main content

This guide takes you from a fresh clone to a running local copy of Reactive Resume, then covers the commands you’ll use while working on it. For how the code is organized, read Project architecture.

Before you start#

You need:

  • Node.js 24. The version is pinned in .nvmrc and the root engines field, so nvm use or fnm use picks it up.
  • pnpm 12. The root packageManager field pins the exact version (currently pnpm@12.11.2), and pnpm switches to it automatically when you run it inside the repository.
  • Docker with Docker Compose for PostgreSQL, Redis, and S3-compatible storage. Start the Docker daemon first.
  • Git.

Set up your checkout#

Run every command from the repository root unless a step says otherwise.

Clone the repository

Install dependencies

The install also sets up the Lefthook Git hooks described in Commits and pull requests.

Start the infrastructure services

This starts:

ServicePurposePort
postgresThe database5432
redisOptional: shared rate limits, resumable assistant replies, live resume events, and view de-duplication6379
seaweedfsS3-compatible storage for uploads8333
seaweedfs_create_bucketCreates the reactive-resume bucket, then exits—

Wait until ps shows the services as healthy. PDF generation runs on the Forme engine in WebAssembly, so you don’t need a Chromium or Browserless container.

Create your environment file

Copy the template only if you don’t have a .env.local yet:

The template uses container hostnames. Because the app runs on your machine, change these values in .env.local to localhost:

Then generate the secrets:

AUTH_SECRET is required. ENCRYPTION_SECRET (at least 32 characters) is needed only for saved AI providers and the assistant, but it’s easiest to set it now. Redis is optional; with it, assistant replies survive a page reload and rate limits are shared between server processes. Every variable is described in Environment variables.

Tip

Working on something that doesn’t need uploads? Start only postgres and set STORAGE_BACKEND=local. Files then go to the data/ folder in your checkout.

Start the app

Open http://localhost:3000. The server applies database migrations on startup, so a fresh database is ready as soon as the app loads.

Create a local account

Select Sign up and create an account. Without SMTP settings, the app doesn’t send email: verification and password-reset links are printed in the terminal running pnpm dev. Copy the link from there into your browser.

What pnpm dev runs#

pnpm dev loads .env.local through dotenvx and starts three processes with Turborepo:

ProcessAddressNotes
Vite (web app)http://localhost:3000 (PORT)Hot reload. Proxies /api, /mcp, /uploads, /.well-known, and /schema.json to the server.
Hono (server)http://localhost:3001 (SERVER_PORT)Restarts on change through tsx watch.
Email previewhttp://localhost:3002Previews the email templates in packages/email.

Use pnpm dev:web to start only Vite. API calls still need a server running.

Everyday commands#

Vite+ supplies the project-local vp CLI for Vite builds/dev servers, Vitest tests, Oxlint, and Oxfmt. Run it with pnpm exec vp; no global install is needed. Turbo still schedules workspace tasks and checks package boundaries, while tsdown packages the server and DeepSeek plugin. Root vite.config.ts owns lint and formatting settings; package vitest.config.ts files retain isolated test configuration.

Keep the pinned vite-plus catalog version and vite override together. The vitest override and @vitest/coverage-v8 must match Vite+‘s bundled Vitest version (pnpm exec vp toolchain vitest). The Vite Plus extension pack uses the same tools and root config as scripts and hooks.

Typechecks use tsrs, an experimental Rust port of TypeScript. The pinned release supports macOS arm64 and Linux x64/arm64 (GNU/musl); Windows and Intel macOS binaries are not published. Existing tsconfig.json strictness settings and --noEmit remain in force. TypeScript stays installed for tools that consume its API and for editors; this migration does not switch editor language servers.

CommandWhat it does
pnpm devStart the web app, server, and email preview
pnpm dev:webStart only the web app
pnpm buildBuild the web app and server for production (regenerates PDF translations first)
NODE_ENV=production pnpm startRun the built server on PORT. It reads exported variables or the root .env, not .env.local.
pnpm typecheckType-check every workspace with tsrs
pnpm testRun the Vitest suites of every workspace
pnpm test:e2eRun the Playwright browser tests (see Browser tests)
pnpm lintCheck code with Oxlint; errors and warnings fail the command
pnpm lint:agentThe same lint check with compact coding-agent diagnostics
pnpm format:checkCheck formatting, import order, and Tailwind class order without changing files
pnpm lint:fixApply safe lint fixes
pnpm formatFormat files and sort imports/Tailwind classes with Oxfmt
pnpm checkChanges files: regenerates PDF translations, applies safe lint fixes, formats, then checks remaining lint findings
pnpm exec turbo boundariesCheck package boundary rules
pnpm knipFind unused files, exports, and dependencies
pnpm lingui:extractExtract new UI strings into apps/web/locales/*.po
pnpm docs:genRegenerate the OpenAPI spec and the custom-styles CSS reference

Prefer package-scoped commands while you work. Package names come from each package.json: the apps are web and server, and shared packages are @reactive-resume/<name>.

Work with the database#

CommandWhat it does
pnpm db:generateGenerate a migration from schema changes
pnpm db:migrateApply pending migrations without starting the app
pnpm db:studioOpen Drizzle Studio at local.drizzle.studio

All three load .env.local before calling Drizzle Kit, which doesn’t read .env files by itself.

To change the schema:

  1. Edit the tables in packages/db/src/schema/*.
  2. Run pnpm db:generate. The migration is written to the root migrations/ folder.
  3. Read the generated SQL, then apply it with pnpm db:migrate or by restarting pnpm dev.
Warning

Review every generated migration before you apply it. Don’t reset the database or delete Docker volumes to work around a setup error; find the cause instead.

Run tests#

Unit and integration tests#

Tests run through vp test using Vitest and sit next to the code they cover as *.test.ts(x) or *.spec.ts(x). Import test APIs from vite-plus/test. Most packages run in Node; packages/ui uses happy-dom.

PDF export text tests require Poppler’s pdftotext to read logical Unicode text, including /ActualText spans for shaped scripts and emoji. Install it with brew install poppler on macOS or sudo apt-get install poppler-utils on Debian/Ubuntu. CI installs it before running the unit suites.

Pass file paths straight after test. An extra -- stops Vitest from filtering the run. Most test scripts use --passWithNoTests, so a green run with zero tests proves nothing about your change.

Database integration suites need COVER_LETTER_TEST_DATABASE_URL, INTEGRATIONS_TEST_DATABASE_URL, and OAUTH_TEST_DATABASE_URL. The migration suite uses COVER_LETTER_TEST_DATABASE_URL as an administrative connection and creates and deletes its own disposable databases, so its user needs CREATE DATABASE permission. Give the OAuth suite its own database, because it writes signing keys. Never point test variables at a database with real data. .github/workflows/ci.yml shows the full setup.

Browser tests#

End-to-end tests use Playwright and live in tests/e2e/specs, with fixtures in tests/e2e/fixtures. They cover sign-up and sign-in, section editing and autosave, JSON export and import, public sharing with statistics and passwords, OAuth consent for MCP clients, and the assistant against a scripted AI provider.

Playwright starts the built server (node apps/server/dist/index.mjs) in production mode and waits for /api/health, so build first. Use a disposable database, and export the variables yourself; these scripts don’t load .env.local.

Without FLAG_ALLOW_UNSAFE_AI_BASE_URL=true, the assistant spec skips itself. Playwright runs Chromium with no retries. Locally it reuses a server that’s already running on PORT. See tests/e2e/README.md for the full recipe.

Note

Keep the unsafe AI and OAuth redirect flags for isolated test installations. They relax SSRF and redirect protections.

Add translatable text#

The web app uses Lingui. Wrap every user-facing string in a macro:

Then run pnpm lingui:extract. It updates the catalogs in apps/web/locales/*.po and regenerates the PDF section-title catalog. You only add English strings; translators handle the rest on Crowdin. See Contributing translations.

Code style#

  • TypeScript is strict, including exactOptionalPropertyTypes and noUncheckedIndexedAccess. Packages type-check with tsrs --noEmit.
  • Oxlint checks code, including native React Compiler and accessibility rules. Oxfmt formats with tabs, double quotes, and 120-column lines. It sorts imports into type, Node, test, external, workspace, and local groups, and sorts Tailwind classes in clsx, cva, and cn. Side-effect imports retain their order. Install the recommended Oxc editor extension for fixes and formatting on save.
  • Configure lint rules in the root vite.config.ts lint block and formatting in its fmt block. Generated files and byte-sensitive CSS fixtures stay excluded. Async test doubles are exempt from require-await; source functions still require it. The Playwright fixture adapter is exempt from rules-of-hooks because its use callback belongs to Playwright. CSS declarations are formatted, but Oxlint does not lint them.
  • @shadcn/lint is registered with Oxlint. Design-system rules are opt-in; add your chosen rules in the root vite.config.ts lint block, scoped to apps/web and packages/ui. Existing components.json files identify the shared component exports and Tailwind theme. See its rules and configuration examples.
  • React components with explicit props use a named props type, such as type FooProps = {...} with function Foo(props: FooProps).

Commits and pull requests#

The Git hooks run automatically:

  • Before each commit, Lefthook checks staged files for merge conflict markers, applies safe Oxlint fixes, runs Oxfmt, then checks remaining lint findings with warnings denied and stages the fixes.
  • On each commit message, commitlint enforces Conventional Commits, such as fix(pdf): keep the timeline dot round or docs: update the development guide.

Before you open a pull request:

  1. Run the type checks and tests for the packages you changed, plus pnpm lint and pnpm format:check.
  2. Run pnpm exec turbo boundaries if you changed imports, exports, or shared contracts.
  3. Run pnpm build if you changed runtime or bundling behavior.
  4. Keep the pull request focused. Describe the problem, the new behavior, and the checks you ran, and link the related GitHub issue.

Pull requests run the CI workflow (.github/workflows/ci.yml) with four parallel checks:

JobWhat it checks
Run unit testsUnit tests for every workspace, affected-package typechecks, and package boundaries. Test concurrency adapts to the runner.
Run browser testsA production build and Playwright browser tests. Browser workers adapt to the runner.
Check Vercel compatibilityBuilds the Vercel artifact offline and checks the backend Function. See Deployment checks.
Check Cloudflare compatibilityChecks the Cloudflare build and runs tests against the local Workers runtime without cloud credentials

The separate autofix.ci workflow (autofix.yml) runs pnpm knip --fix and pnpm check, then pushes any fixes to your branch. The autofix.ci service requires its own workflow name.

New commits to a pull request cancel its superseded CI and autofix runs. Docker publishing and mobile release workflows run separately.

Keep credentials and personal resume data out of code, logs, test fixtures, issues, and pull requests.

Troubleshooting#

Port 3000 or 3001 is already in use

Stop the other process, or set different values for PORT and SERVER_PORT in .env.local and change APP_URL to match. Keep port 3002 free for the email preview.

The database connection is refused

Check that the containers are healthy with docker compose -f compose.dev.yml ps. Code on your machine connects to localhost; code inside a container uses the service name, such as postgres.

Uploads fail with S3 errors

Read the storage logs:

Check that S3_ENDPOINT is http://localhost:8333 and the bucket exists. If you don’t need S3, set STORAGE_BACKEND=local.

Type errors about routes after pulling or adding a route

apps/web/src/routeTree.gen.ts is generated. Start pnpm dev (or run pnpm build) to regenerate it, and never edit it by hand.

AI providers are unavailable

Saved AI providers and the assistant need ENCRYPTION_SECRET (at least 32 characters). Set it in .env.local and restart pnpm dev.

A server dependency fails to load on Vercel

CommonJS server dependencies must be bundled. Add the package and its dependencies to bundledInteropPackages in apps/server/tsdown.config.ts. See Deployment checks.

Next steps#