Skip to content

Development Setup

Karthik P edited this page Oct 15, 2026 · 1 revision

Development Setup

This page explains how to run Job Matcher from a checkout: the tools you need, the bootstrap commands, what bun run dev starts and on which ports, how hot reload and the fake providers work, the Prisma workflow, the git hooks, the Biome and TypeScript settings, and every package.json script in the workspace. For the layout of the code itself, see Repository Layout; for tests, see Testing.

Prerequisites

Tool Version Why
Bun 1.4 (CI installs bun-v1.4.3; the Docker images use oven/bun:1.4) Package manager, script runner, test runner, and the runtime for the API, worker and Next.js
Docker with Compose v2 Any current release Runs PostgreSQL 17 and Redis 8 for development, and optionally a self-hosted Firecrawl
Git Any bun install points git at the repository's hooks
Chromium for Playwright Installed with bun x playwright install --with-deps chromium Only for bun run test:e2e

CI also installs Node.js 26.11 next to Bun (ci.yml). No other global tools are needed: Biome, Prisma, TypeScript and Playwright are workspace dev dependencies.

Bootstrap

git clone https://github.com/Karthik-beta/AI-Job-Resume-Matcher.git
cd AI-Job-Resume-Matcher
cp .env.example .env          # then set BETTER_AUTH_SECRET, e.g. to: openssl rand -base64 32
bun install                   # also runs prisma generate and sets core.hooksPath
docker compose up -d --wait   # Postgres and Redis, so the migration can connect
bun run db:migrate            # applies prisma/migrations and regenerates the client
bun run dev

The README lists bun run db:migrate before bun run dev. Because bun run dev is what starts Postgres, start the containers first (as above) on a fresh machine, or the migration has no database to connect to.

For demo data with no API keys, set PROVIDERS=fake in .env before starting. With PROVIDERS=live the worker needs OPENROUTER_API_KEY and a Firecrawl key or local URL; see Configuration Reference. When .env.example gains a new variable, copy it into your .env.

What bun run dev starts

The root script is:

docker compose up -d --wait && bun --filter '*' dev
  1. docker compose up -d --wait starts the services in docker-compose.yml and waits for their healthchecks.
  2. bun --filter '*' dev runs the dev script of every workspace that has one: apps/web (Next.js) and apps/api (API and worker). packages/shared has no dev script.

Docker Compose services (development)

Service Image Profile Host port Healthcheck Notes
postgres postgres:17 default ${POSTGRES_PORT:-5432} pg_isready -U postgres -d job_matcher every 2 s User and password postgres, database job_matcher, volume postgres-data
redis redis:8 default 6379 redis-cli ping every 2 s No persistence volume
firecrawl-api ghcr.io/firecrawl/firecrawl:2.11.506 firecrawl 3002 curl -fsS http://localhost:3002/v0/health/readiness, 30 retries, 20 s start period USE_DB_AUTHENTICATION=false (no API key needed), 4 CPUs, 8 GB memory limit, passes SEARXNG_* through
firecrawl-playwright ghcr.io/firecrawl/playwright-service (pinned digest) firecrawl none none 2 CPUs, 4 GB, all capabilities dropped, no-new-privileges
firecrawl-redis redis:8.10.2-alpine firecrawl none redis-cli ping Separate from the app's Redis
firecrawl-rabbitmq rabbitmq:3.13.7-management firecrawl none Waits for the Erlang cookie, then rabbitmq-diagnostics -q check_running
firecrawl-postgres ghcr.io/firecrawl/nuq-postgres (pinned digest) firecrawl none pg_isready -U postgres -d postgres Firecrawl's job queue, separate from the app's database

The Firecrawl services start only when the firecrawl profile is active. Set it in .env, which Compose reads:

COMPOSE_PROFILES=firecrawl
FIRECRAWL_MODE=local          # or hybrid
FIRECRAWL_LOCAL_URL=http://localhost:3002

The Firecrawl images take about 6 GB of disk, the API is ready in about 95 seconds once pulled, and it uses about 3.5 GB of memory under light scraping (configuration.md). Because bun run dev waits on every healthcheck, the first start with the profile takes that long. To stop only Firecrawl:

docker compose --profile firecrawl stop firecrawl-api firecrawl-playwright firecrawl-redis firecrawl-rabbitmq firecrawl-postgres

Processes and ports

Process Command URL or port Hot reload
Web next dev --port 3000 http://localhost:3000 Next.js Fast Refresh (Turbopack, with the @tailwindcss/turbopack loader for CSS)
API bun --env-file=../../.env --watch src/main.ts http://localhost:4000/api (from API_PORT) bun --watch restarts on changes to imported files
Worker bun --env-file=../../.env --watch src/worker.ts none (connects to Redis) bun --watch
Postgres Docker localhost:5432
Redis Docker localhost:6379
Firecrawl (optional) Docker http://localhost:3002

The API's dev script is bun run --parallel --no-exit-on-error dev:server dev:worker, so a crash in one process does not stop the other. The web app proxies /api/* to API_URL (default http://localhost:4000), so the browser only talks to port 3000.

Health endpoints:

BETTER_AUTH_URL must match the address in your browser. The default .env.example value http://localhost:3000 works for local development; opening http://127.0.0.1:3000 instead fails sign-in with an invalid origin error.

Fake providers

With PROVIDERS=fake, the worker builds fakeProvidersLayer instead of the live one:

Service Fake behaviour
Firecrawl Serves markdown from apps/api/test/fixtures/. A page asked for links returns listing-page.md; pages on jobs.example.com return jobs/<path>.md; any other page returns resume.md. FIRECRAWL_MODE picks fake cloud, local or hybrid instances, so each mode can be tried. The fake cloud plan reports 1,000 credits and 20 days left.
Scripted errors ?fake-error=401, 402 or 429 on a URL blocks Firecrawl (stops the run); any other status fails just that scrape. ?fake-blocked makes the local instance return a bot check, so hybrid runs retry on the cloud.
Search Every search returns two fixture job pages.
Listing extractor Returns listings.json.
Resume Every resume URL returns resume.md.
HTTP fetch and PDF parser Always fail, so ATS boards and PDF downloads do not work in fake mode.
Evaluator Reads the job page's Verdict: line: match is a match, auth fails with an OpenRouter 401, invalid fails with invalid output, anything else is no match. Preflight always passes.
Title triage Scores every title 3, keeping all jobs in listing order.
Discord Logs "Fake Discord alert for " when the webhook URL is valid.

The API tests and the Playwright suite run on these fakes. See Testing and Glossary: fake providers.

Prisma workflow

File Purpose
prisma/schema.prisma Data model. The generator is prisma-client with runtime = "bun", moduleFormat = "esm", output apps/api/src/generated/prisma (git-ignored).
prisma.config.ts Prisma 7 config: schema path, migrations path prisma/migrations, and datasource.url from env("DATABASE_URL"). The datasource block in the schema has no URL.
prisma/migrations/ SQL migrations, one folder per change, and migration_lock.toml.

The API connects through the driver adapter @prisma/adapter-pg (prisma.service.ts).

Task Command
Change the schema and create a migration Edit schema.prisma, then bun run db:migrate (runs prisma migrate dev, which prompts for a migration name, then db:generate)
Regenerate the client only bun run db:generate (also runs on every bun install through postinstall)
Apply existing migrations without creating new ones (CI, e2e, production) bun x --bun prisma migrate deploy

prisma generate needs DATABASE_URL to be set because the config reads it, but it never connects; the Dockerfile sets a placeholder for this reason. In production the migrate service runs bunx --bun prisma migrate deploy before the API and worker start (Deployment). The data model is described in Data Model.

Git hooks

bun install runs the root prepare script, git config core.hooksPath .githooks, so the hooks in .githooks/ run for every commit.

Hook What it does
pre-commit bun run lint && bun run typecheck
commit-msg Rejects a message with a Co-Authored-By: trailer or a line starting with "Generated with" (case-insensitive), ignoring comment lines and anything below the git commit -v scissors line. Merge commits (when MERGE_HEAD exists) are skipped. It does not enforce a message format; see Contributing.

Biome

biome.json (Biome 2.5) lints and formats TypeScript, JavaScript, JSON and CSS across the whole repository, including site/. It follows .gitignore and skips **/test/fixtures/**/*.html.

Setting Value
Formatter Spaces, indent 2, line width 100
Parsers Parameter decorators enabled (for NestJS); Tailwind directives in CSS
Rule preset recommended
complexity.useLiteralKeys off (allows env["KEY"], needed by noPropertyAccessFromIndexSignature)
suspicious.noExplicitAny error
suspicious.noConsole error (use the Nest Logger or Effect logging)
style.noNonNullAssertion error
style.useImportType error, except in apps/api/src/**, where Nest's dependency injection needs value imports of injected classes
nursery.noFloatingPromises error (mark intentional fire-and-forget calls with void)
apps/web/components/ui/** a11y.noLabelWithoutControl, a11y.useSemanticElements, suspicious.noArrayIndexKey and suspicious.noDoubleEquals off (generated shadcn components)
Assist Organize imports on
bun run lint     # biome check .
bun run format   # biome check --write .

TypeScript

All packages extend tsconfig.base.json (TypeScript 6). Nothing is emitted: Bun and Next.js run the sources directly, and tsc only checks types.

Option Value Effect
target, lib ES2023 Language level
module, moduleResolution ESNext, bundler
moduleDetection force Every file is a module
isolatedModules, verbatimModuleSyntax true Type-only imports must use import type
strict true
noUncheckedIndexedAccess true Indexing an array or record yields T | undefined
exactOptionalPropertyTypes true An optional property cannot be assigned undefined unless its type says so
noImplicitOverride true
noImplicitReturns true
noFallthroughCasesInSwitch true
noPropertyAccessFromIndexSignature true Index-signature keys need bracket access, for example process.env["API_URL"]
useUnknownInCatchVariables true
skipLibCheck, resolveJsonModule, noEmit true

Package overrides:

Package Additions
apps/api types: ["bun"], experimentalDecorators, emitDecoratorMetadata (NestJS); includes src and test
apps/web lib adds DOM and DOM.Iterable, jsx: react-jsx, incremental, the next plugin, path alias @/* to the app root
packages/shared lib adds DOM

Scripts

Script Command Description
dev docker compose up -d --wait && bun --filter '*' dev Start the containers, then the web app, API and worker
typecheck bun --filter '*' typecheck Type-check every workspace
lint biome check . Lint and format check
format biome check --write . Apply fixes and formatting
db:migrate bun --bun prisma migrate dev Create and apply migrations
postdb:migrate bun run db:generate Runs after db:migrate
db:generate bun --bun prisma generate Generate the Prisma client
test bun --filter '*' test Unit and integration tests in apps/api and apps/web
test:e2e playwright test Playwright end-to-end tests
check bun run lint && bun run typecheck && bun run test && bun run test:e2e Everything CI's check job runs
postinstall prisma generate Generate the client after install
prepare git config core.hooksPath .githooks Install the git hooks

apps/api (package.json)

Script Command Description
dev bun run --parallel --no-exit-on-error dev:server dev:worker API and worker with watch mode
dev:server bun --env-file=../../.env --watch src/main.ts API only
dev:worker bun --env-file=../../.env --watch src/worker.ts Worker only
start bun --env-file=../../.env src/main.ts API without watch (used by Playwright)
start:worker bun --env-file=../../.env src/worker.ts Worker without watch (used by Playwright)
test bun --env-file=../../.env test --timeout 30000 API tests with coverage thresholds (bunfig.toml)
typecheck tsc --noEmit

apps/web (package.json)

Script Command Description
dev next dev --port 3000 Development server
build next build Production build
start next start Serve the production build (port 3000)
test bun test Unit tests in lib/
typecheck next typegen && tsc --noEmit Generates route types, then checks

packages/shared (package.json)

Script Command
typecheck tsc --noEmit

Previewing the marketing site

site/ is static HTML, CSS and JavaScript with no build step:

python -m http.server 5180 --directory site

See Marketing Site.

Building the production images locally

cp deploy/.env.example deploy/.env   # fill in the Required section
docker compose -f deploy/compose.yml -f deploy/compose.build.yml up -d --build --wait

This is what CI's docker job does with PROVIDERS=fake. See Deployment.

Clone this wiki locally