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
.nvmrcand the rootenginesfield, sonvm useorfnm usepicks it up. - pnpm 12. The root
packageManagerfield pins the exact version (currentlypnpm@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:
| Service | Purpose | Port |
|---|---|---|
postgres | The database | 5432 |
redis | Optional: shared rate limits, resumable assistant replies, live resume events, and view de-duplication | 6379 |
seaweedfs | S3-compatible storage for uploads | 8333 |
seaweedfs_create_bucket | Creates 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.
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:
| Process | Address | Notes |
|---|---|---|
| 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 preview | http://localhost:3002 | Previews 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.
| Command | What it does |
|---|---|
pnpm dev | Start the web app, server, and email preview |
pnpm dev:web | Start only the web app |
pnpm build | Build the web app and server for production (regenerates PDF translations first) |
NODE_ENV=production pnpm start | Run the built server on PORT. It reads exported variables or the root .env, not .env.local. |
pnpm typecheck | Type-check every workspace with tsrs |
pnpm test | Run the Vitest suites of every workspace |
pnpm test:e2e | Run the Playwright browser tests (see Browser tests) |
pnpm lint | Check code with Oxlint; errors and warnings fail the command |
pnpm lint:agent | The same lint check with compact coding-agent diagnostics |
pnpm format:check | Check formatting, import order, and Tailwind class order without changing files |
pnpm lint:fix | Apply safe lint fixes |
pnpm format | Format files and sort imports/Tailwind classes with Oxfmt |
pnpm check | Changes files: regenerates PDF translations, applies safe lint fixes, formats, then checks remaining lint findings |
pnpm exec turbo boundaries | Check package boundary rules |
pnpm knip | Find unused files, exports, and dependencies |
pnpm lingui:extract | Extract new UI strings into apps/web/locales/*.po |
pnpm docs:gen | Regenerate 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#
| Command | What it does |
|---|---|
pnpm db:generate | Generate a migration from schema changes |
pnpm db:migrate | Apply pending migrations without starting the app |
pnpm db:studio | Open 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:
- Edit the tables in
packages/db/src/schema/*. - Run
pnpm db:generate. The migration is written to the rootmigrations/folder. - Read the generated SQL, then apply it with
pnpm db:migrateor by restartingpnpm 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 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.
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
exactOptionalPropertyTypesandnoUncheckedIndexedAccess. Packages type-check withtsrs --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, andcn. 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.tslint block and formatting in itsfmtblock. Generated files and byte-sensitive CSS fixtures stay excluded. Async test doubles are exempt fromrequire-await; source functions still require it. The Playwright fixture adapter is exempt fromrules-of-hooksbecause itsusecallback belongs to Playwright. CSS declarations are formatted, but Oxlint does not lint them. @shadcn/lintis registered with Oxlint. Design-system rules are opt-in; add your chosen rules in the rootvite.config.tslint block, scoped toapps/webandpackages/ui. Existingcomponents.jsonfiles 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 = {...}withfunction 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 roundordocs: update the development guide.
Before you open a pull request:
- Run the type checks and tests for the packages you changed, plus
pnpm lintandpnpm format:check. - Run
pnpm exec turbo boundariesif you changed imports, exports, or shared contracts. - Run
pnpm buildif you changed runtime or bundling behavior. - 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:
| Job | What it checks |
|---|---|
| Run unit tests | Unit tests for every workspace, affected-package typechecks, and package boundaries. Test concurrency adapts to the runner. |
| Run browser tests | A production build and Playwright browser tests. Browser workers adapt to the runner. |
| Check Vercel compatibility | Builds the Vercel artifact offline and checks the backend Function. See Deployment checks. |
| Check Cloudflare compatibility | Checks 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.
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.