Repository navigation
Repository Layout
Job Matcher is a Bun workspace monorepo with two applications and one shared package. apps/api holds the NestJS API and the BullMQ run worker, which share one codebase and one Docker image. apps/web is the Next.js frontend. packages/shared holds the Effect schemas both apps use to validate requests. Around them sit the Prisma schema and migrations, the Playwright suite, the Docker and Compose files for self-hosting, the installers, the static marketing site, and the documentation. This page is an annotated tree of the repository, followed by each workspace package's dependencies. For how the pieces interact at runtime, see Architecture.
.
├── .github/workflows/
│ ├── ci.yml Lint, typecheck, tests, e2e, Docker stack and installer test; image publishing on tags
│ └── pages.yml Deploys site/ to GitHub Pages
├── .githooks/
│ ├── pre-commit bun run lint && bun run typecheck
│ └── commit-msg Rejects Co-Authored-By and "Generated with" lines
├── apps/
│ ├── api/ NestJS API and run worker (one image)
│ └── web/ Next.js 16 frontend
├── packages/
│ └── shared/ Request schemas and types shared by both apps
├── prisma/
│ ├── schema.prisma Data model; client generated into apps/api/src/generated/prisma
│ └── migrations/ 12 SQL migrations (20261010084657_init ... 20261013075002_onboarding_state)
├── e2e/ Playwright specs and shared steps
├── deploy/
│ ├── compose.yml Self-hosted stack on the published images
│ ├── compose.build.yml Override that builds the images from this checkout
│ ├── .env.example Self-hosted settings template
│ └── README.md
├── docs/
│ ├── configuration.md Scraping modes, sources, filters, credits, OpenRouter routing
│ ├── self-hosting.md Install, upgrade, HTTPS, backups, troubleshooting
│ ├── brand/ Mark, lockups, app icon and brand guide (PDF)
│ ├── screenshots/ README and site screenshots
│ ├── logo.svg
│ └── social-preview.png
├── site/ Static marketing page (no build step), published to GitHub Pages
├── Dockerfile Multi-stage build: base, api and web targets
├── .dockerignore
├── docker-compose.yml Development Postgres and Redis, plus the optional firecrawl profile
├── install.sh One-line installer for macOS, Linux and WSL
├── install.ps1 One-line installer for Windows PowerShell
├── package.json Workspace root: scripts and shared dev dependencies
├── bun.lock
├── biome.json Lint and format rules
├── tsconfig.base.json Strict compiler options every package extends
├── prisma.config.ts Prisma 7 config: schema, migrations path, DATABASE_URL
├── playwright.config.ts e2e config: isolated database, fake providers, web servers
├── .env.example Development settings template
├── .gitattributes LF line endings everywhere; PDFs binary
├── CONTRIBUTING.md
├── LICENSE MIT
└── README.md
The API (src/main.ts) and the worker (src/worker.ts) are two entry points into the same source tree. The API boots AppModule as an HTTP server with the global prefix /api; the worker boots WorkerModule as an application context with no HTTP server. See Backend API and Run Pipeline.
apps/api/
├── bunfig.toml Test preload and coverage thresholds (90% lines, 75% functions per file)
├── package.json
├── tsconfig.json Adds Bun types and decorator metadata
├── src/
│ ├── main.ts API entry: createApp(), listen on API_PORT
│ ├── worker.ts Worker entry: NestFactory.createApplicationContext(WorkerModule)
│ ├── app.ts createApp(): Nest app with bodyParser off and the /api prefix (shared with tests)
│ ├── app.module.ts API module: controllers, services, Prisma, Redis, auth and queue modules
│ ├── config.ts Startup environment schema (Effect Schema); see Configuration Reference
│ ├── auth/
│ │ ├── auth.ts better-auth instance: email and password, Prisma adapter, BETTER_AUTH_URL
│ │ └── auth.module.ts Registers better-auth with @thallesp/nestjs-better-auth
│ ├── cache/
│ │ ├── cache.controller.ts DELETE /api/cache
│ │ └── cache.service.ts Clears the signed-in user's page cache
│ ├── common/
│ │ ├── schema.pipe.ts Nest pipe that validates a body or query with an Effect schema
│ │ └── timeout.ts withTimeout() for Redis calls that would otherwise wait for a reconnect
│ ├── health/
│ │ └── health.controller.ts GET /api/health (database, Redis) and /api/health/worker
│ ├── matches/
│ │ ├── matches.controller.ts GET /api/matches, PATCH /api/matches/:id
│ │ └── matches.service.ts Cursor-paged listing, newest first; status updates
│ ├── matching/ The Effect pipeline the worker runs
│ │ ├── classify-scrape.ts Decides whether a scraped page is usable, blocked or failed
│ │ ├── completion.ts OpenRouter chat completions: flex routing, back-off, usage logging
│ │ ├── context-key.ts sha256 of resume hash, sources, model, requirements and title filters
│ │ ├── credit-guard.ts Firecrawl credit reserve and scheduled runs' daily allowance
│ │ ├── dedupe.ts Canonical job URLs (tracking parameters removed) and deduplication
│ │ ├── discord.ts Discord webhook notifier, with Retry-After handling
│ │ ├── download.ts HTTP fetch service with timeouts and a size limit
│ │ ├── errors.ts Tagged errors and stopsRun(): which failures stop a run
│ │ ├── evaluate.ts Match evaluator: resume and job page in, verdict and reason out
│ │ ├── firecrawl.ts Firecrawl SDK client, rate limiters, credit usage
│ │ ├── job-posting.ts Reads schema.org JobPosting data from a job page
│ │ ├── listing-extractor.ts LLM extraction of listings from a careers page
│ │ ├── match-output.ts Strict JSON schema and decoding of the evaluator's reply
│ │ ├── page-cache.ts PageCache store: resumes, job pages and source listings, with TTLs
│ │ ├── pdf.ts PDF text extraction (LiteParse), with OCR for scanned files
│ │ ├── preflight.ts Checks the OpenRouter key and the model's endpoints before a run
│ │ ├── requirements.ts Location and experience filters
│ │ ├── resume-source.ts Fetches and hashes the resume (PDF download or scrape)
│ │ ├── retry-after.ts Parses Retry-After hints
│ │ ├── routing.ts OPENROUTER_* routing settings
│ │ ├── scrape-router.ts FIRECRAWL_* settings; cloud, local and hybrid routing; local health check
│ │ ├── scrape-routes.ts ScrapeRoute store: sites that block the local Firecrawl
│ │ ├── title-filters.ts Whole-word include and exclude title keywords
│ │ ├── triage.ts Title triage: scores titles 0 to 3 in batches of 150
│ │ ├── modules.test.ts Loads every matching module so coverage counts each one
│ │ ├── *.test.ts Unit tests next to each module
│ │ └── sources/
│ │ ├── adapter.ts SourceAdapter interface and helpers
│ │ ├── registry.ts SourceKind to adapter map
│ │ ├── detect.ts Detects the ATS kind and board key from a URL
│ │ ├── embedded.ts Finds ATS boards embedded in a careers page's HTML
│ │ ├── greenhouse.ts Greenhouse public API adapter
│ │ ├── lever.ts Lever public API adapter
│ │ ├── ashby.ts Ashby public API adapter
│ │ ├── smartrecruiters.ts SmartRecruiters adapter (newest 500 postings)
│ │ ├── recruitee.ts Recruitee adapter
│ │ ├── page.ts Careers page adapter: links across "next" pages, optional site map
│ │ ├── page-links.ts Reads job links and titles from page markdown
│ │ ├── search.ts Search source adapter (Firecrawl /search over ATS hosts, past week)
│ │ ├── http.ts Per-host request spacing (1.5 s) and JSON fetch for ATS APIs
│ │ ├── text.ts HTML job ads to plain text
│ │ └── *.test.ts
│ ├── onboarding/
│ │ ├── onboarding.controller.ts POST /api/onboarding/complete, /dismiss, /restore
│ │ ├── onboarding.service.ts Sets and clears the onboarding timestamps
│ │ └── onboarding.ts Derives the steps and next step from the user's data
│ ├── overview/
│ │ ├── overview.controller.ts GET /api/overview
│ │ └── overview.service.ts Home page data: setup, latest and active run, pending matches, next tick
│ ├── prisma/
│ │ ├── prisma.module.ts
│ │ └── prisma.service.ts PrismaClient over @prisma/adapter-pg
│ ├── providers/
│ │ ├── live.ts Live layer: Firecrawl, OpenRouter, Discord, HTTP, PDF
│ │ └── fake.ts Fixture-backed layer for PROVIDERS=fake
│ ├── queue/
│ │ └── queue.module.ts BullMQ root connection and the "runs" queue
│ ├── redis/
│ │ ├── redis.module.ts Global module
│ │ └── redis.service.ts ioredis client with maxRetriesPerRequest: null
│ ├── runs/
│ │ ├── runs.controller.ts GET /api/runs, GET /api/runs/:id, POST /api/runs, POST /api/runs/:id/results/:resultId/save
│ │ ├── runs.service.ts Starts runs, lists runs, retries a failed match save
│ │ ├── create-run.ts Creates a run under a per-user advisory lock
│ │ ├── schedule.ts syncSchedule() and enqueueRun()
│ │ ├── runs.queue.ts Queue name, job types, scheduler id user:<userId>
│ │ ├── runs.processor.ts BullMQ worker (concurrency 1): run jobs and schedule ticks
│ │ ├── process-run.ts The run pipeline: preflight, sources, filters, triage, evaluation, alerts
│ │ ├── recover-run.ts Ends runs whose queue job was lost or interrupted
│ │ ├── run-settings.ts Settings row to requirements and title filters
│ │ ├── save-match.ts Upserts a Match by canonical URL
│ │ ├── match-alerts.ts Discord alerts sent from the API (retried saves)
│ │ └── run-page-url.ts Run page URL on BETTER_AUTH_URL, for alerts
│ ├── settings/
│ │ ├── settings.controller.ts GET and PUT /api/settings
│ │ └── settings.service.ts Upserts settings and syncs the user's job scheduler
│ ├── sources/
│ │ ├── sources.controller.ts GET/POST /api/sources, POST /api/sources/search, PATCH/DELETE /api/sources/:id
│ │ └── sources.service.ts Adds, detects, switches and deletes sources
│ ├── worker/
│ │ ├── worker.module.ts Worker module: Prisma, Redis, queue, EffectRuntime, RunsProcessor
│ │ └── effect-runtime.ts ManagedRuntime with the live or fake layer, built at boot
│ └── generated/prisma/ Prisma client output (git-ignored)
└── test/
├── setup.ts Preload: fake providers, deletes provider keys, blocks real provider hosts
├── helpers.ts startApp() and signUp() for supertest integration tests
├── ats.ts Fake HTTP replies and ATS fixtures for adapter tests
├── pages.ts Careers page markdown fixtures
├── *.test.ts Integration tests against Postgres and Redis (16 files)
└── fixtures/ ATS JSON, careers pages, job ads, scrape outcomes, resume, listings, a PDF
A Next.js App Router application. Signed-in pages sit in the (app) route group, which wraps them in AppShell. Data is fetched from the API through relative /api paths with TanStack Query. See Frontend, Components and Visual System.
apps/web/
├── next.config.ts Rewrites /api/:path* to API_URL; Turbopack Tailwind loader; cacheComponents
├── proxy.ts Next.js proxy: redirects by session cookie presence
├── components.json shadcn configuration (style base-nova, lucide icons)
├── package.json
├── tsconfig.json DOM libs, JSX, @/* alias
├── public/ Install icons: icon-192.png, icon-512.png, icon-maskable-512.png
├── app/
│ ├── layout.tsx Root layout
│ ├── providers.tsx Client provider for the TanStack Query client
│ ├── globals.css Tailwind 4 and design tokens
│ ├── page.tsx Landing page (/)
│ ├── manifest.ts Web app manifest
│ ├── icon.svg, apple-icon.png, favicon.ico
│ ├── sign-in/page.tsx /sign-in
│ ├── sign-up/page.tsx /sign-up
│ └── (app)/
│ ├── layout.tsx Wraps every signed-in page in AppShell
│ ├── home/page.tsx /home: overview and setup checklist
│ ├── welcome/page.tsx /welcome: setup wizard (?step=)
│ ├── analyze/page.tsx /analyze: pick sources, start a run, latest and past runs
│ ├── runs/[id]/page.tsx /runs/:id: run details (?first=1 adds the run guide)
│ ├── matches/page.tsx /matches: pending and applied matches
│ └── settings/page.tsx /settings: all settings sections
├── components/
│ ├── app-shell.tsx Header, primary navigation, account menu, session guard
│ ├── app-page.tsx Page body inside the app layout
│ ├── header-nav.tsx Landing page header links
│ ├── logo.tsx The mark and wordmark
│ ├── auth-page.tsx, auth-form.tsx Sign-in and sign-up
│ ├── home-view.tsx /home content
│ ├── setup-checklist.tsx "Get set up" checklist on /home
│ ├── welcome-view.tsx, welcome-steps.tsx Setup wizard
│ ├── analyze-view.tsx /analyze content
│ ├── run-card.tsx Run status, counts, progress and job results
│ ├── run-details-view.tsx /runs/:id content
│ ├── first-run-explainer.tsx "What happens during a run" guide
│ ├── past-runs.tsx Runs before the latest
│ ├── example-run.tsx Illustrative run on the landing page
│ ├── verdict-badge.tsx Match, no match, skipped and error badges with explanations
│ ├── scoring-help.tsx How a job becomes a match
│ ├── retry-save-button.tsx Retries a failed match save
│ ├── matches-view.tsx /matches content
│ ├── settings-view.tsx /settings content and section navigation
│ ├── settings-section.tsx Shared section, form and field helpers
│ ├── resume-section.tsx Resume
│ ├── job-sources.tsx Job sources
│ ├── matching-section.tsx Matching
│ ├── run-section.tsx Runs (jobs per run, triage)
│ ├── schedule-section.tsx Schedule & alerts
│ ├── clear-cache.tsx Data (clear cache)
│ └── ui/ shadcn components on Base UI: alert-dialog, badge, button, card,
│ dropdown-menu, field, input, label, progress, separator, skeleton,
│ sonner, switch
└── lib/
├── api.ts Fetch wrapper for /api and ApiError
├── auth-client.ts better-auth React client
├── overview.ts, runs.ts, matches.ts, settings.ts, onboarding.ts Query hooks per resource
├── safe-next.ts Validates ?next= redirects; AFTER_SIGN_IN (/home), AFTER_SIGN_UP (/welcome)
├── welcome-steps.ts Wizard steps and navigation rules
├── serial.ts Runs queued tasks one at a time
├── user-flags.ts Per-user flags kept in the browser
├── relative-time.ts "3 hours ago" formatting
├── validate.ts, utils.ts
└── *.test.ts safe-next, serial and welcome-steps unit tests
packages/shared/src/index.ts is a single module exported as @job-matcher/shared (the package's exports maps . to the TypeScript source, so there is no build step). The API validates request bodies with these schemas through SchemaPipe, and the web app uses the same schemas and types for its forms and responses, so both sides enforce identical rules.
| Export | Kind | Purpose |
|---|---|---|
HttpUrl |
Schema | Trimmed http or https URL |
isDiscordWebhookUrl |
Function |
https URL on a Discord host with a /api/webhooks/ path |
CreateJobSource |
Schema |
{ url } for POST /api/sources
|
CreateSearchSource, maxSearchQueryLength (200) |
Schema, constant |
{ query } for POST /api/sources/search
|
SourceKind, atsKinds, sourceKindLabels
|
Schema, set, record | The seven source kinds, which are ATS boards, and their display names |
UpdateJobSource |
Schema |
{ kind, adapterKey, useMap? } for PATCH /api/sources/:id
|
StartRun |
Schema |
{ sourceIds } (non-empty) for POST /api/runs
|
MatchStatus, MatchesQuery, UpdateMatch
|
Schemas |
PENDING/APPLIED, cursor paging, status update |
maxTitleKeywords (30), maxTitleKeywordLength (60) |
Constants | Title filter limits |
UpdateSettings, defaultSettings
|
Schema, value | The settings form and its defaults |
RunStatus |
Schema |
QUEUED, RUNNING, COMPLETED, STOPPED, FAILED
|
OnboardingStep, Onboarding
|
Schemas | Setup steps and state |
Overview |
Schema |
GET /api/overview response |
Its only dependency is effect.
Versions are the ranges declared in each package.json.
| Dev dependency | Version | Use |
|---|---|---|
@biomejs/biome |
^2.5.15 |
Lint and format |
@playwright/test |
1.63.0 (pinned) |
End-to-end tests |
@types/bun |
^1.4.2 |
Bun types |
prisma |
^7.10.0 |
Prisma CLI |
typescript |
^6.0.3 |
Type checking |
| Dependency | Version | Use |
|---|---|---|
@nestjs/common, @nestjs/core, @nestjs/platform-express
|
^12.1.2 |
HTTP framework |
@nestjs/bullmq |
^12.0.0 |
Queue integration |
bullmq |
^6.3.12 |
Run queue and job schedulers |
ioredis |
^6.0.0 |
Redis client |
@prisma/client, @prisma/adapter-pg
|
^7.10.0 |
Database access |
better-auth |
^1.7.7 |
Authentication |
@thallesp/nestjs-better-auth |
^2.8.0 |
better-auth guard, @Session() and @AllowAnonymous() for Nest |
effect |
^4.0.2 |
Pipeline, schemas, config validation |
@effect/ai-openrouter |
4.0.2 (pinned) |
OpenRouter client |
@mendable/firecrawl-js |
^4.46.0 |
Firecrawl SDK |
@llamaindex/liteparse |
2.15.1 (pinned) |
PDF parsing |
reflect-metadata |
^0.2.2 |
Decorator metadata |
rxjs |
^7.8.2 |
Nest peer dependency |
@job-matcher/shared |
workspace:* |
Shared schemas |
supertest, @types/supertest (dev) |
^7.3.1, ^7.2.1
|
HTTP integration tests |
| Dependency | Version | Use |
|---|---|---|
next |
^16.4.0 |
Framework |
react, react-dom
|
^19.3.0 |
UI |
@tanstack/react-query |
^5.104.1 |
Data fetching and caching |
better-auth |
^1.7.7 |
Auth client |
@base-ui/react |
^1.9.0 |
Headless UI primitives |
shadcn |
^4.21.4 |
Component tooling |
class-variance-authority |
^0.7.1 |
Component variants |
cn |
^0.4.0 |
Class name helper |
lucide-react |
^1.55.0 |
Icons |
next-themes |
^0.4.6 |
Light and dark theme |
sonner |
^2.0.8 |
Toasts |
tw-animate-css |
^1.4.0 |
Animations |
effect |
^4.0.2 |
Shared schemas |
@job-matcher/shared |
workspace:* |
Shared schemas |
tailwindcss, @tailwindcss/turbopack (dev) |
^4.3.3 |
Styling |
@types/node, @types/react, @types/react-dom (dev) |
^26.6.5, ^19.3.0, ^19.3.0
|
Types |
| Dependency | Version |
|---|---|
effect |
^4.0.2 |
flowchart LR
web["@job-matcher/web"] --> shared["@job-matcher/shared"]
api["@job-matcher/api"] --> shared
api --> prisma["Prisma client (generated from prisma/schema.prisma)"]
shared --> effect["effect"]
web --> effect
api --> effect
| You want to change | Look in |
|---|---|
| A request body's validation | packages/shared/src/index.ts |
| An HTTP endpoint |
apps/api/src/<resource>/ controller and service, registered in app.module.ts
|
| The run pipeline |
apps/api/src/runs/process-run.ts and apps/api/src/matching/
|
| A job board adapter | apps/api/src/matching/sources/ |
| The data model |
prisma/schema.prisma plus a new migration |
| A page |
apps/web/app/ and its view in apps/web/components/
|
| Colours and tokens |
apps/web/app/globals.css (see Design Tokens) |
| The self-hosted stack |
deploy/ and Dockerfile
|
See Contributing for step-by-step guidance.
Job Matcher · Website · Report an issue · MIT license
Using Job Matcher
System design
Matching engine
Interface and brand
Engineering