Repository navigation
Configuration Reference
Job Matcher is configured in two places. Environment variables, set in .env, configure each process at startup: the database, Redis, authentication, provider keys, scraping mode and LLM routing. Per-user settings, edited on the Settings page and stored in PostgreSQL, configure what each user's runs look for: resume, locations, experience, title filters, run size, triage, schedule and Discord alerts. This page lists every variable and every setting, with its default, when it is required, what it does, and which service reads it. For background on the scraping modes and credit rules, see Firecrawl Credits and Caching and AI Matching.
| Context | File | How it is loaded |
|---|---|---|
| Development |
.env at the repository root, copied from .env.example
|
The API scripts run bun --env-file=../../.env (apps/api/package.json). Docker Compose also reads it for POSTGRES_PORT, COMPOSE_PROFILES and the SEARXNG_* values. |
| Self-hosted |
.env next to compose.yml, from deploy/.env.example
|
deploy/compose.yml passes the whole file to migrate, api and worker with env_file: .env, and overrides DATABASE_URL and REDIS_URL. The web container gets no environment from .env. |
| CI | Job-level env: in ci.yml
|
See CI/CD and Releases. |
-
apps/api/src/config.ts decodes the core variables with an Effect
Schemawhen the module is first imported. Both the API (src/main.ts) and the worker (src/worker.ts) import it, so both processes exit at startup ifDATABASE_URL,API_PORT,BETTER_AUTH_SECRET,BETTER_AUTH_URLorREDIS_URLis missing or empty, or ifPROVIDERSis notliveorfake. - Provider variables are read only by the worker, and only when
PROVIDERS=live(providers/live.ts). The worker builds its provider layers when it boots (worker/effect-runtime.ts), so a missing key or bad value stops the worker at startup, not mid-run. The API process never reads them. - For the Firecrawl and OpenRouter routing variables, an empty value (a
KEY=line) counts as unset and takes the default (scrape-router.ts, routing.ts). This does not apply toOPENROUTER_MODELor the core variables, which must be non-empty if present.
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
DATABASE_URL |
none (dev example: postgresql://postgres:postgres@localhost:5432/job_matcher) |
Always | PostgreSQL connection string for Prisma (@prisma/adapter-pg). Also read by prisma.config.ts for migrations. In deploy/compose.yml it is set from POSTGRES_PASSWORD and need not be in .env. Playwright appends _e2e to its database name. |
API, worker, Prisma CLI, Playwright |
API_PORT |
none (examples: 4000) |
Always | Port the NestJS API listens on. Parsed as a number. The production api healthcheck and the web image's API_URL both assume 4000, so leave it at 4000 in the self-hosted stack. |
API, worker (validated only) |
BETTER_AUTH_SECRET |
none | Always | Secret better-auth uses to sign session cookies and tokens. Use a long random value, for example openssl rand -hex 32. Changing it signs every user out. |
API, worker (validated only) |
BETTER_AUTH_URL |
none (dev: http://localhost:3000) |
Always | The exact origin users open, passed to better-auth as baseURL. Sign-in and sign-up from any other origin fail with an invalid origin error. Must include the port, and the scheme (https:// behind a TLS proxy). Also the origin of the run-page links in Discord alerts (run-page-url.ts). |
API, worker |
REDIS_URL |
none (dev: redis://localhost:6379) |
Always | Redis connection for BullMQ and the health check. Set to redis://redis:6379 by deploy/compose.yml. |
API, worker |
PROVIDERS |
live in code; fake in deploy/.env.example; live in .env.example
|
Optional |
live uses Firecrawl, OpenRouter and Discord. fake swaps all three for fixtures (demo mode, tests). See Glossary: fake providers. |
API (validated), worker |
These are read by Docker Compose itself, from .env next to deploy/compose.yml.
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
POSTGRES_PASSWORD |
none | Always (Compose fails with "Set POSTGRES_PASSWORD in .env") | Password of the postgres user, used to initialise the database volume and to build DATABASE_URL for migrate, api and worker. It only takes effect when the volume is first created, so changing it later locks the app out of an existing database. |
Compose (postgres, migrate, api, worker) |
WEB_PORT |
8080 |
Optional | Host port published for the web container (${WEB_PORT}:3000). The installer picks the first free port from 8080. Change BETTER_AUTH_URL to match. |
Compose (web) |
JOB_MATCHER_VERSION |
latest |
Optional | Image tag for ghcr.io/karthik-beta/job-matcher-api and -web, for example 1.0.0. The installer writes the release it installed. |
Compose |
The installer itself reads JOB_MATCHER_DIR, JOB_MATCHER_VERSION, JOB_MATCHER_REF and JOB_MATCHER_NONINTERACTIVE from your shell; see Getting Started.
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
OPENROUTER_API_KEY |
none |
PROVIDERS=live (worker exits without it) |
Key for every LLM call: evaluation, listing extraction and title triage. A management or provisioning key is refused by the run's preflight. | Worker |
OPENROUTER_MODEL |
openai/gpt-6-luna:floor |
Optional | Any OpenRouter model id. Part of each run's context key, so changing it re-evaluates jobs. CI uses nvidia/nemotron-3-super-120b-a12b:free. |
API (stored in the run's context key), worker |
OPENROUTER_SERVICE_TIER |
flex |
Optional | One of auto, default, flex, priority, scale. With flex, preflight requires a flex endpoint, and a reply served on another tier is logged as a warning. |
Worker |
OPENROUTER_PROVIDERS |
openai |
Optional | Comma-separated providers a request may be routed to. | Worker |
OPENROUTER_MAX_PRICE_PROMPT |
0.05 |
Optional | Price cap in USD per million prompt tokens (OpenRouter provider.max_price). |
Worker |
OPENROUTER_MAX_PRICE_COMPLETION |
0.25 |
Optional | Price cap in USD per million completion tokens. | Worker |
OPENROUTER_REASONING_EFFORT |
low |
Optional | One of max, xhigh, high, medium, low, minimal, none. |
Worker |
An invalid value for a routing variable stops the worker at startup with a schema decode error. See AI Matching for preflight and the flex back-off schedule.
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
FIRECRAWL_MODE |
cloud |
Optional |
cloud (every scrape on Firecrawl's cloud), local (a self-hosted Firecrawl, no credits) or hybrid (local first, cloud only for blocked pages). Any other value stops the worker: "FIRECRAWL_MODE must be cloud, local or hybrid, and FIRECRAWL_CLOUD_PROXY basic or auto." In demo mode it also picks which fake instances are used. |
Worker |
FIRECRAWL_API_KEY |
none |
cloud and hybrid
|
Firecrawl cloud key. Missing: "FIRECRAWL_MODE=cloud needs FIRECRAWL_API_KEY." | Worker |
FIRECRAWL_LOCAL_URL |
none (.env.example: http://localhost:3002) |
local and hybrid
|
Base URL of the self-hosted Firecrawl; trailing slashes are removed. Must be http or https. Missing: "FIRECRAWL_MODE=local needs FIRECRAWL_LOCAL_URL (e.g. http://localhost:3002)." |
Worker |
FIRECRAWL_CLOUD_PROXY |
basic |
Optional | Cloud mode's proxy: basic, or auto (retries blocked pages with enhanced proxies). Hybrid mode always uses auto for its cloud fallback. |
Worker |
FIRECRAWL_CREDIT_RESERVE |
50 |
Optional (cloud, hybrid) |
Cloud credits every run leaves untouched on the plan. Must be a whole number: "FIRECRAWL_CREDIT_RESERVE must be a whole number of credits, got ...". Scheduled runs also keep to an even daily share of what is above it. See Glossary: credit reserve. | Worker |
FIRECRAWL_LOCAL_SEARCH |
false |
Optional (local) |
true when the self-hosted Firecrawl has SEARXNG_ENDPOINT set, so search sources can run in local mode. Only true or false (case-insensitive) are accepted. Without it, search sources are skipped in local mode with a warning. |
Worker |
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
POSTGRES_PORT |
5432 |
Optional | Host port for the dev Postgres container in docker-compose.yml. Update the port in DATABASE_URL to match. Redis is always published on 6379. |
Compose (dev) |
COMPOSE_PROFILES |
unset (commented out in .env.example) |
To run a self-hosted Firecrawl in dev |
firecrawl makes docker compose up (and so bun run dev) also start the Firecrawl services on port 3002. |
Compose (dev) |
SEARXNG_ENDPOINT |
empty | Optional | Passed to the dev firecrawl-api container. A SearXNG instance with JSON output, used for search in local mode. Empty means Firecrawl falls back to scraping DuckDuckGo, which the app does not rely on. |
Compose (dev firecrawl-api) |
SEARXNG_ENGINES |
empty | Optional | Passed to firecrawl-api. |
Compose (dev firecrawl-api) |
SEARXNG_CATEGORIES |
empty | Optional | Passed to firecrawl-api. |
Compose (dev firecrawl-api) |
| Variable | Default | Required when | Effect | Read by |
|---|---|---|---|---|
API_URL |
http://localhost:4000 in dev; build argument http://api:4000 in the Docker image |
Optional | Where the Next.js rewrite sends /api/:path* (next.config.ts). Rewrites are resolved at build time, so in the image it is a build argument (--build-arg API_URL=...); setting it on a running container has no effect. |
Web (build time) |
NODE_ENV |
production in both images |
Set by the Dockerfile | Standard Node/Next.js production mode. | API, web |
NEXT_TELEMETRY_DISABLED |
1 in the web image |
Set by the Dockerfile | Turns off Next.js telemetry. | Web |
The browser never needs the API's address: apps/web/lib/api.ts calls relative /api/... paths, which the web server proxies. This keeps the session cookie first-party. See Frontend.
| Variable | Default | Effect | Read by |
|---|---|---|---|
CI |
unset | In playwright.config.ts: one retry per test, an HTML report, a production build of the web app instead of next dev, and no reuse of a running web server. |
Playwright |
PROVIDERS, DATABASE_URL
|
set by Playwright | The e2e API and worker get PROVIDERS=fake and DATABASE_URL with _e2e appended to the database name. |
Playwright |
FIRECRAWL_API_KEY, OPENROUTER_API_KEY, FIRECRAWL_MODE
|
deleted |
apps/api/test/setup.ts deletes them and sets PROVIDERS=fake before any test file loads. |
API tests |
See Testing.
| Variable | .env.example |
deploy/.env.example |
deploy/compose.yml |
docker-compose.yml |
|---|---|---|---|---|
DATABASE_URL |
yes | no (built by compose) | set for migrate/api/worker
|
no |
POSTGRES_PORT |
yes | no | no | yes |
POSTGRES_PASSWORD |
no | yes (required) | yes | no (fixed postgres) |
API_PORT |
yes | yes | no | no |
BETTER_AUTH_SECRET, BETTER_AUTH_URL
|
yes | yes (required) | via env_file
|
no |
REDIS_URL |
yes | no (built by compose) | set | no |
WEB_PORT, JOB_MATCHER_VERSION
|
no | yes | yes | no |
PROVIDERS |
yes (live) |
yes (fake) |
via env_file
|
no |
OPENROUTER_* |
yes | yes | via env_file
|
no |
FIRECRAWL_MODE, FIRECRAWL_LOCAL_URL, FIRECRAWL_LOCAL_SEARCH
|
yes | no (cloud only by default; may be added) | via env_file
|
no |
FIRECRAWL_API_KEY, FIRECRAWL_CLOUD_PROXY, FIRECRAWL_CREDIT_RESERVE
|
yes | yes | via env_file
|
no |
COMPOSE_PROFILES, SEARXNG_*
|
commented | no | no | yes |
The self-hosted stack has no Firecrawl services. To use local or hybrid mode there, run a Firecrawl instance separately and add FIRECRAWL_MODE and FIRECRAWL_LOCAL_URL to .env. The URL must be reachable from inside the worker container, so localhost does not work; use a host name or address the container can reach.
Each user has one Settings row (prisma/schema.prisma), created on the first save. Until then the API returns defaultSettings from packages/shared/src/index.ts. The Settings page saves with PUT /api/settings (settings.controller.ts); the body is validated by the shared UpdateSettings schema, so the web form and the API enforce the same rules.
| Field | Settings section and label | Default | Validation | Effect |
|---|---|---|---|---|
resumeUrl |
Resume: "Resume URL" | null |
null or an http/https URL |
The resume every job is checked against: a public PDF (Google Drive and Dropbox share links work) or a page. A run cannot start without it ("Add a resume URL in settings before starting a run"). Its content hash is part of the context key, so a new file at the same URL is evaluated again. |
experienceYears |
Matching: "Experience in years" | 0 |
Integer, 0 or more | Your experience. A job whose minimum experience exceeds years plus months is filtered out. |
experienceMonths |
Matching: "And months" | 0 |
Integer 0 to 11 ("Months must be between 0 and 11") | Added to years as a fraction of 12. |
targetMinYears |
Matching: "Target minimum years" | null |
null or a number 0 or more |
Drops jobs whose maximum experience is below it. |
targetMaxYears |
Matching: "Target maximum years" | null |
null or a number 0 or more, at least targetMinYears ("Maximum years must be at least the minimum") |
Drops jobs whose minimum experience is above it. |
locations |
Matching: "Cities" (comma-separated, e.g. Bengaluru, Remote) |
[] |
Array of strings | When not empty, a job must mention one of these cities as a whole word in one of its locations (case-insensitive). Empty means any location. |
includeUnknown |
Matching: "Include listings with unknown experience or location" | true |
Boolean | Whether a job that states no location (when cities are set) or no experience range (when a target range is set) is kept. A job page is read before it is excluded for missing details. |
titleInclude |
Matching: "Job titles must include" | [] |
Up to 30 keywords, each 1 to 60 characters after trimming | When not empty, a title must contain one of these words or phrases, matched as whole words. |
titleExclude |
Matching: "Job titles must not include" | [] |
Same as titleInclude
|
A title must contain none of these. |
maxJobsPerRun |
Runs: "Jobs to check per run" | 5 |
Integer 1 to 20 ("Jobs per run must be between 1 and 20") | The run size: how many jobs one run reads and evaluates. Jobs already evaluated in the same context are skipped, so the next run continues with the rest. |
triageEnabled |
Runs: "Rank job titles before checking jobs" | true |
Boolean | Title triage: one model request per 150 titles scores each title 0 to 3 against the resume, and only titles scoring 2 or more are read, best first. See Glossary: triage. |
scheduleMinutes |
Schedule & alerts: "Check every N minutes" | null |
null or an integer of 15 or more ("Use 15 minutes or more") |
Scheduled checks. Saving creates or removes a BullMQ job scheduler (user:<userId>) that starts a SCHEDULED run over all of the user's sources every N minutes. Empty means manual runs only. |
discordWebhookUrl |
Schedule & alerts | null |
null or an https URL on discord.com, discordapp.com, ptb.discord.com or canary.discord.com with a path under /api/webhooks/ ("Enter a Discord webhook URL") |
Each saved match is posted to this webhook. A failed post is logged and never fails the job. A stored value that no longer passes the check shows "Alerts are not being sent" on the form. |
Two more columns hold onboarding state and are not editable on the form: onboardingCompletedAt (set when the wizard is finished or the first run starts) and onboardingDismissedAt (set when the setup checklist is hidden). They are changed through POST /api/onboarding/complete, /dismiss and /restore. See Data Model.
Each JobSource row has its own settings (schema.prisma, UpdateJobSource in packages/shared):
| Field | Default | Editable in the UI | Effect |
|---|---|---|---|
url |
none | Set when adding | The board, careers page, or for a search a generated URL. Unique per user. |
kind |
Detected from the URL (PAGE otherwise) |
Yes (PATCH /api/sources/:id) |
GREENHOUSE, LEVER, ASHBY, SMARTRECRUITERS, RECRUITEE, PAGE; SEARCH only through POST /api/sources/search. A source cannot be switched to SEARCH. Changing the kind clears its cached listings. |
adapterKey |
Detected | Yes ("Board name") | The ATS board token or company slug. Required for ATS kinds; letters, numbers, dots, dashes and underscores. |
useMap |
false |
Yes, page sources only | Read job links from Firecrawl's site map (1 credit) before the page, falling back to the page. |
query |
null |
Set when adding a search | The search text, 1 to 200 characters. |
selector |
null |
No (database only) | CSS selector limiting which part of a page source is read for job links. |
maxPages |
3 |
No (database only) | How many "next" pages of a page source's listings to follow, counting the first. |
suggestedKind, suggestedAdapterKey
|
null |
Shown as a suggestion | An ATS board found embedded in a page source, offered as a switch. |
| Change | Development | Self-hosted |
|---|---|---|
| An environment variable | Edit .env and restart bun run dev; --watch restarts the API and worker on source changes, and .env is read when the process starts. |
Edit .env and run docker compose up -d, which recreates the containers whose configuration changed. |
| A user setting | Settings page; takes effect on the next run. | Same. |
API_URL |
Restart next dev. |
Rebuild the web image with --build-arg API_URL=.... |
Job Matcher · Website · Report an issue · MIT license
Using Job Matcher
System design
Matching engine
Interface and brand
Engineering