# Contributing to Jobify Thanks for your interest in improving Jobify! Bug reports, importer requests and pull requests are all welcome. For larger changes, please open an issue first so we can agree on the approach. ## Getting started ```sh npm install npm run dev ``` The API runs on port 3000 and the web app on , which proxies `/api` to the API. The development database is created in `apps/server/data/`. Copy `.env.example` to `.env` to set environment variables locally. Every pull request runs the same checks in CI, plus a build of the Docker image, and nothing is published until they pass. To catch problems before you push, run: ```sh npm run lint npm run format:check npm run typecheck npm test ``` ## Project layout ``` apps/ server/ NestJS API (also serves the built web app) drizzle/ SQL migrations, generated by drizzle-kit src/ jobs/ Jobs and their timeline events importers/ Turning job posting URLs or HTML into job drafts ai/ AI providers, prompts and AI-powered features stats/ Dashboard numbers backup/ JSON/CSV export and JSON import auth/ Sign-in, sessions and the guard that protects every route users/ Accounts, profiles and user management settings/ Key/value settings stored in the database database/ Drizzle schema and connection web/ React single-page app src/ api/ Typed API client, response types and TanStack Query hooks components/ Shared components (ui/ holds the generic building blocks) pages/ One component per route lib/ Formatting, labels and small hooks ``` In production the server serves `apps/web/dist` and everything under `/api` is the JSON API. ## Changing the database schema 1. Edit `apps/server/src/database/schema.ts`. 2. Run `npm run db:generate -w apps/server` and commit the new files in `apps/server/drizzle/`. Migrations run automatically when the server starts. ## Adding a job site importer Importers live in `apps/server/src/importers/parsers/`. Each one implements `JobParser`: ```ts export class ExampleParser implements JobParser { readonly id = 'example'; readonly site = 'Example Jobs'; matches(url: URL | undefined): boolean { return url?.hostname === 'jobs.example.com'; } async parse(page: PageContext): Promise { const posting = await page.fetchJson(`https://api.example.com/jobs/${id}`); return { title: cleanText(posting.title), description: htmlToMarkdown(posting.descriptionHtml), // ...whatever else the site provides }; } } ``` Then add it to `JOB_PARSERS` in `parsers/index.ts`, above the generic parsers. If the site has no API but renders postings with stable markup (as Indeed and Glassdoor do), add a profile to `parsers/rendered-page.parser.ts` instead: a list of selectors for the title, company, location and description. Profiles are how pages pasted from a browser are read. Some tips: - Prefer a site's public JSON API over scraping HTML; it is far more stable. `page.fetchJson()` and `page.fetchText()` fetch other URLs, while `page.html()` and `page.document()` (cheerio) give you the posting page itself and are only downloaded if you call them. - Only return fields you are confident about. Parsers run in order and later ones fill in whatever is still missing, so leaving the company name to the page's structured data is better than guessing it from a URL slug. - Use the helpers in `importers/normalize.ts` for Markdown conversion, employment types, workplace types, dates and salaries. - Add a spec next to your parser with a trimmed-down sample response. `importers/testing.ts` has a `fakePage()` helper so tests never hit the network. - You do not need to extract everything: after the parsers run, `importers/enrich.ts` reads the salary, location, remote/hybrid and employment type out of the description when they are missing, and `text-signals.ts` has the shared heuristics for that. ## Adding an AI provider Providers implement `AiProvider` in `apps/server/src/ai/providers/`. Add the provider's metadata to `AI_PROVIDERS` in `ai/ai-settings.ts` (this drives the settings form) and construct it in `ai/provider-factory.ts`. ## Code style - TypeScript everywhere, formatted with Prettier and linted with ESLint. - Keep modules focused; prefer small, well-named functions over comments explaining large ones. - The web app keeps server state in TanStack Query. Add new endpoints to `api/hooks.ts` rather than calling `fetch` from components. ## Releasing Releases are published from GitHub's **Releases** page: 1. Choose **Draft a new release**. 2. Under **Choose a tag**, type a new version such as `v1.2.3` (with the `v`) and pick `main` as the target. 3. Click **Generate release notes**, edit them if you like, and **Publish release**. The CI workflow then runs the checks against the tagged commit and, only if they pass, builds the image and publishes `:1.2.3`, `:1.2` and `:latest`, which Unraid and other installs pick up as an update. Tick **Set as a pre-release** to publish a test build under its version tag without moving `:latest`. Every push to `main` also publishes `:edge`. The version shown in the app comes from the release tag, so there is no need to bump the version in `package.json`. ## License By contributing you agree that your contributions are licensed under the [GNU Affero General Public License v3.0](LICENSE).