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 [email protected]), 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.
1

Clone the repository

2

Install dependencies

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

Start the infrastructure services

This starts: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.
4

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.
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.
5

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.
6

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: Use pnpm dev:web to start only Vite. API calls still need a server running.

Everyday commands

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

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.
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 use Vitest and sit next to the code they cover as *.test.ts(x) or *.spec.ts(x). 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/e2e.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.
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 tsgo --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 .oxlintrc.json and formatting in .oxfmtrc.json. 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 .oxlintrc.json, 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.
  • Coding agents should run pnpm exec oxlint --fix <changed paths>, then pnpm exec oxfmt <changed paths>, and finish with pnpm lint:agent and pnpm format:check. Follow the Oxc coding-agent workflow; fix findings and give any necessary inline suppression a rule-specific explanation.
  • 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 these GitHub Actions workflows: Keep credentials and personal resume data out of code, logs, test fixtures, issues, and pull requests.

Troubleshooting

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.
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.
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.
apps/web/src/routeTree.gen.ts is generated. Start pnpm dev (or run pnpm build) to regenerate it, and never edit it by hand.
Saved AI providers and the assistant need ENCRYPTION_SECRET (at least 32 characters). Set it in .env.local and restart pnpm dev.
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

Project architecture

Learn where each part of the code lives and where new code belongs.

GitHub repository

Browse the source, open issues, and send pull requests.