Repository navigation
Contributing
This page describes how changes reach master: the branch and pull request workflow, the commit message style the history follows, the rules for stacked pull requests, the code style the hooks and CI enforce, and step-by-step guidance for the most common additions (a job source adapter, an API endpoint, a design token). It expands on CONTRIBUTING.md. To get a local setup running first, see Development Setup.
-
Branch from
master. Branch names in the history use a type prefix and, when there is one, the Jira key:feat/AJM-96-onboarding-flow,fix/AJM-108-111-review-findings,ci/github-actions,feat/marketing-site. -
Make focused commits (see Commit messages). The pre-commit hook runs
bun run lintandbun run typecheckon every commit. -
Run the full suite before opening a pull request:
bun run check
This is lint, typecheck, unit and integration tests, and the Playwright suite, the same as CI's
checkjob. Changes toDockerfile,deploy/or the installers are also exercised by CI'sdockerjob; to try them locally, see Deployment. -
Open a pull request against
master(or against the branch it builds on; see Stacked pull requests). -
The pull request needs a passing
checkjob before it can merge.masteris protected for administrators too, and force pushes and deletion are blocked. See CI/CD and Releases.
Pull requests are merged with merge commits (Merge pull request #106 from Karthik-beta/ci/github-actions), not squashed, so each commit on the branch stays in the history.
The commit-msg hook enforces one rule: a message may not contain a Co-Authored-By: trailer or a line starting with "Generated with". Merge commits skip the check. It does not enforce a format, but the history consistently follows Conventional Commits:
<type>(<scope>): <summary in lower case, imperative mood> (<Jira key>)
| Part | Values seen in the history |
|---|---|
| Type |
feat, fix, test, docs, ci, merge (for recorded merges between feature branches) |
| Scope (optional) |
api, web, site, install, brand
|
| Summary | Imperative, lower case, no trailing period, describing the effect for the user: "end QUEUED runs that never got their job" |
| Jira key (optional) | In parentheses at the end: (AJM-109), or a range (AJM-104 – AJM-107)
|
Examples from git log:
feat(web): use the new mark for the logo, favicons and install icons
fix(api): recover lost runs only when they block a start (AJM-109)
fix(install): keep .env readable by its owner only (AJM-108)
test(install): check that an upgrade tightens a world-readable .env (AJM-108)
ci: use only GitHub-owned actions in the CI workflow
docs: link the website from the README
A pull request can target another feature branch when it builds on unmerged work. GitHub closes a pull request whose base branch is deleted, and a closed pull request of that kind cannot be reopened, so a stack must be merged bottom-up and retargeted before any base branch is deleted:
-
Merge the bottom pull request into
master. Keep its branch for now. -
Change the base of the next pull request to
master:gh pr edit <number> --base master
If GitHub reports conflicts, merge
masterinto that branch. -
Only then delete the merged branch.
-
Repeat for each pull request up the stack.
Before merging, check that the base shown at the top of the pull request is master, so it does not land in another feature branch.
| Area | Rule | Enforced by |
|---|---|---|
| Formatting | 2-space indent, 100-character lines, organised imports | Biome (bun run format fixes it) |
| Types |
strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature and the other flags in tsconfig.base.json
|
bun run typecheck |
No any
|
noExplicitAny is an error; decode unknown input with an Effect Schema
|
Biome |
No console
|
Use the Nest Logger in Nest code and Effect.log* in the pipeline |
Biome |
| No non-null assertions | Handle the undefined case |
Biome |
| No floating promises | Await it, or mark fire-and-forget with void
|
Biome |
| Type-only imports |
import type everywhere except apps/api/src, where Nest needs value imports for dependency injection |
Biome, verbatimModuleSyntax
|
| Environment access | Bracket form, process.env["NAME"]; core variables go through config.ts
|
TypeScript |
| Effect at the edges | The matching pipeline (scraping, retries, evaluation, notifications) uses Effect with tagged errors from matching/errors.ts; controllers and services stay plain async code. They meet in worker/effect-runtime.ts. |
Review |
| Failure policy | Every new failure is a tagged error, and stopsRun() decides on purpose whether it stops the run or fails only the job |
Review, errors.test.ts
|
| Shared validation | Request bodies are validated with schemas in packages/shared, used by both the API (SchemaPipe) and the web forms |
Review |
| Comments | Explain why, in full sentences; the codebase avoids restating what code does | Review |
| Tests | 90% of lines and 75% of functions per loaded file in the API |
bunfig.toml coverage thresholds |
| Docs | User-facing copy and docs use plain English and present tense | Review |
See Testing for test conventions.
ATS adapters read a board's public API with no Firecrawl credits. Adding one touches these places:
| # | File | Change |
|---|---|---|
| 1 | prisma/schema.prisma | Add the kind to enum SourceKind, then bun run db:migrate to create the migration |
| 2 | packages/shared/src/index.ts | Add it to SourceKind, to atsKinds (if it is read through a public API with a board key) and to sourceKindLabels
|
| 3 | apps/api/src/matching/sources/<name>.ts |
Implement SourceAdapter from adapter.ts: fetchListings(source, previous) returns listings (use boardKey() for the token and the JSON helper and per-host spacing in sources/http.ts); add fetchDescription and describes if the API can return one job's ad |
| 4 | registry.ts | Add the adapter to the adapters record. The record is typed Record<SourceKind, SourceAdapter>, so typecheck fails until you do. |
| 5 | detect.ts | Detect the kind and board key from the board's URL host and path |
| 6 | embedded.ts | Optional: recognise the board when a careers page embeds it, so it is offered as a switch |
| 7 | apps/api/test/setup.ts | Add the ATS host to blockedHosts, so no test can call it |
| 8 | apps/api/test/fixtures/ats/ |
Record a real API response as a fixture |
| 9 | apps/api/src/matching/sources/<name>.test.ts |
Test listings, descriptions, errors and detection with fakeHttp() from test/ats.ts
|
| 10 | Docs | Add the board to the README, docs/configuration.md and Job Sources and Scraping
|
Check the provider's robots.txt and terms before adding an adapter; existing adapters note when this was checked.
| # | Where | Change |
|---|---|---|
| 1 | packages/shared/src/index.ts |
Add the request schema (and response schema if the web app decodes it) |
| 2 | apps/api/src/<resource>/<resource>.controller.ts |
Add the route. Read the user with @Session() session: UserSession and validate input with @Body(new SchemaPipe(Schema)) or @Query(new SchemaPipe(Schema)). Routes require a session by default through @thallesp/nestjs-better-auth; only health checks use @AllowAnonymous(). |
| 3 | apps/api/src/<resource>/<resource>.service.ts |
Implement it with PrismaService, always scoping queries by userId
|
| 4 | app.module.ts | Register a new controller and service |
| 5 | apps/api/test/<resource>.test.ts |
Integration tests with startApp() and signUp(): the happy path, "rejects requests without a session", and another user's data being hidden |
| 6 | apps/web/lib/<resource>.ts |
A TanStack Query hook that calls api("/<path>")
|
The global prefix /api is added by createApp(), so a controller declares @Controller("matches"), not @Controller("api/matches"). If the endpoint queues work for the worker, follow the pattern in runs/: write the database row first, then enqueue with the row id as the job id. See Backend API.
Tokens live in apps/web/app/globals.css:
- Add the value as a CSS variable in
:root, for example--signal: #...;. - Expose it to Tailwind in the
@theme inlineblock, for example--color-signal: var(--signal);. - Use the generated utilities (
bg-signal,text-signal,border-signal) in components.
Follow Design Tokens and Visual System for naming and contrast, and Brand for the logo colours.
| Change | Where |
|---|---|
| A user setting |
prisma/schema.prisma (column with a default) and a migration, UpdateSettings and defaultSettings in packages/shared, the relevant section component in apps/web/components/, and its use in runs/run-settings.ts or the pipeline. If it changes which jobs match, add it to the context key in matching/context-key.ts. |
| An environment variable | Core variables in apps/api/src/config.ts; provider variables next to their reader (routing.ts, scrape-router.ts). Add it to .env.example, to deploy/.env.example if self-hosters need it, and to Configuration Reference. |
| A page |
apps/web/app/(app)/<route>/page.tsx with its view in apps/web/components/, the route in appSections in apps/web/proxy.ts, and a nav item in app-shell.tsx if it belongs in the header |
| The marketing site |
site/; copy changed screenshots from docs/screenshots/ into site/assets/. See Marketing Site. |
Releases are cut from master with gh release create vX.Y.Z --generate-notes; the tag triggers image publishing. The full procedure is in CI/CD and Releases.
Job Matcher · Website · Report an issue · MIT license
Using Job Matcher
System design
Matching engine
Interface and brand
Engineering