Skip to content

Repository Layout

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

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.

Top level

.
├── .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

apps/api

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

apps/web

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

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.

Workspace packages and dependencies

Versions are the ranges declared in each package.json.

Root (job-matcher)

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

@job-matcher/api

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

@job-matcher/web

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

@job-matcher/shared

Dependency Version
effect ^4.0.2

Dependency graph

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
Loading

Where things live

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.

Clone this wiki locally