Repository navigation
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.
| 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.
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 devThe 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.
The root script is:
docker compose up -d --wait && bun --filter '*' dev-
docker compose up -d --waitstarts the services in docker-compose.yml and waits for their healthchecks. -
bun --filter '*' devruns thedevscript of every workspace that has one:apps/web(Next.js) andapps/api(API and worker).packages/sharedhas nodevscript.
| 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:3002The 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| 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:
-
http://localhost:4000/api/health reports
databaseandredisasupordown(HTTP 503 if either is down). -
http://localhost:4000/api/health/worker reports whether a worker is connected to the
runsqueue (HTTP 503 if not).
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.
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.
| 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.
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.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 .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
|
Root (package.json)
| 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 |
site/ is static HTML, CSS and JavaScript with no build step:
python -m http.server 5180 --directory siteSee Marketing Site.
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 --waitThis is what CI's docker job does with PROVIDERS=fake. See Deployment.
Job Matcher · Website · Report an issue · MIT license
Using Job Matcher
System design
Matching engine
Interface and brand
Engineering