Contributing
Local setup, the scripts that matter, and what CI will check
FirstDevJob is a Next.js frontend and a Convex backend in one repository, with Jest tests and two GitHub Actions workflows. Getting productive means running two dev processes side by side and knowing the five commands CI will run against your branch.
The package manager is Bun, pinned to bun@1.4.2 in package.json and in both workflows. Use bun and bunx; do not use npm, npx, yarn or pnpm — a different lockfile will fail bun install --frozen-lockfile in CI.
Local setup
1. Install
git clone https://github.com/<owner>/firstdevjob.git
cd firstdevjob
bun installpostinstall runs fumadocs-mdx, which generates the .source directory the docs pages import. If /docs fails to resolve its content, re-run bun install or bunx fumadocs-mdx.
2. Configure environment
cp .env.example .env.local.env.example lists every variable name the app reads. You need a Clerk application (publishable key, secret key, and a JWT template named convex) and a Convex project. Convex-side values such as CLERK_JWT_ISSUER_DOMAIN are not read from .env.local — they belong to the Convex deployment:
bunx convex env set CLERK_JWT_ISSUER_DOMAIN https://your-issuer.clerk.accounts.devCLERK_JWT_ISSUER_DOMAIN is the issuer domain only, never the JWKS URL. Email variables can be left unset locally: convex/resend.ts defaults to test mode, and the notification actions log and return a reason instead of failing when EMAIL_FROM is missing.
3. Run both processes
# Terminal 1 — Convex: pushes functions and schema on save, writes
# CONVEX_DEPLOYMENT and NEXT_PUBLIC_CONVEX_URL into .env.local on first run
bunx convex dev
# Terminal 2 — Next.js
bun run devKeep bunx convex dev running while you work on anything in convex/: it regenerates convex/_generated/ and type errors in the app will otherwise be stale.
Optionally seed the tag vocabulary by running the internal mutation seed.seedTags from the Convex dashboard.
Scripts
| Script | What it does |
|---|---|
bun run dev | Next.js dev server with Turbopack |
bun run build | Production build |
bun run start | Serve the production build |
bun run lint | ESLint over the repo (eslint .) |
bun run lint:fix | ESLint with --fix |
bun run format / format:check | Prettier write / check |
bun run test | Jest, one pass |
bun run test:watch | Jest in watch mode |
bun run test:coverage | Jest with coverage, enforces the thresholds |
bun run test:ci | What CI runs: jest --ci --coverage --watchAll=false |
bun run audit:deps | bun audit at high severity with three known advisories ignored |
bun run analyze | Production build with the bundle analyzer |
bun run sync:prod-to-dev | Destructive data copy — see Deployment |
Do not use bun run type-check in a pre-push check or in a script: it is tsc --noEmit --watch and never exits. For a one-shot type check run bunx tsc --noEmit, which is what both workflows use.
Before you push
Run the same five checks CI runs, in this order:
bun run lint # ESLint
bunx tsc --noEmit # TypeScript, one pass
bun run test:coverage # Jest with coverage thresholds
bun run build # Build verification
bun run audit:deps # Dependency audit, high severity and aboveFix every failure before pushing. A red Master blocks the production deploy for everyone, because deploy.yml runs the same suite before it will push Convex functions.
What CI checks
One workflow, .github/workflows/ci.yml, runs on every pull request and every push to Master, with four jobs:
| Job | Contents |
|---|---|
quality | bun run lint, bun x tsc --noEmit, bun run test:ci, Codecov upload (non-blocking) |
build | bun run build with the public Convex and Clerk variables |
security | bun run audit:deps |
dependency-review | Pull requests only; fails on high-severity dependency changes |
Runs are grouped per PR with cancel-in-progress, so pushing a new commit cancels the previous run. .github/workflows/deploy.yml is the production pipeline and is described in Deployment.
Tests
Jest with ts-jest and the jsdom environment; configuration is in jest.config.js, setup in tests/setup/jest.setup.ts.
tests/
├── components/ # React Testing Library tests for src/components
├── convex/ # Pure backend logic: applicationIntegrity, jobSubmissionSecurity
├── lib/ # postJobError, textHighlight
├── setup/ # Global setup and matchers
├── utils/ # Render helpers
└── mocks/ # Static asset stubName files *.test.ts / *.test.tsx under tests/. Coverage is collected from src/** (excluding layouts, loading, error, not-found, src/proxy.ts and type declarations) and enforced globally:
| Metric | Threshold |
|---|---|
| Branches | 36% |
| Functions | 28% |
| Lines | 33% |
| Statements | 33% |
bun run test:coverage fails if any metric drops below its threshold, so deleting tests is as likely to break the build as adding untested code. These numbers are intentionally a floor to ratchet upwards, not a target.
Backend logic that can be unit tested lives in plain modules for exactly this reason — convex/applicationIntegrity.ts and convex/jobSubmissionSecurity.ts export pure functions with no Convex context, and are tested directly. When you add a backend rule, put the rule in a pure helper and call it from the query or mutation.
Making a change
- Branch from
Master. - Keep the change small and focused; match the surrounding code rather than introducing a new pattern.
- Add or update tests for anything with logic in it.
- Update the docs in
content/docs/when behaviour, arguments or limits change. - Run the five pre-push checks.
- Open a pull request describing what changed and why, and link any related issue.
Code conventions
- TypeScript everywhere; no
anyescapes past ESLint. - Function components with hooks; client components marked
"use client". - Tailwind CSS v4 with the CSS variables defined in
globals.css— no inline style objects for themable values. - Import app code through the
@/alias; import Convex throughconvex/_generated/api. - Validate arguments with Convex validators (
v.*) and check auth withrequireAuthorrequireModOrAdminat the top of the handler. - Never commit secrets.
.env.localis git-ignored and stays that way.
Contributing to these docs
Documentation is MDX in content/docs/, rendered by Fumadocs.
- Add a file, then list its slug in that directory's
meta.json— a slug with no file is a broken sidebar link, and a file with no slug is invisible. - Frontmatter
titleanddescriptionare rendered as the page heading by the template. Do not repeat the title as an H1 in the body; start with a summary paragraph and use H2 for sections. Callout,CardandCardsare available without an import, along with aMermaidcomponent for diagrams:
<Mermaid
title="What the diagram shows"
chart={`
flowchart TD
a["Step one"] --> b["Step two"]
`}
/>Quote any label containing punctuation, and do not put backticks or HTML line breaks inside the chart.
- Link between pages with absolute
/docs/...URLs. - Check names, arguments and limits against the source before writing them down. Every function, field, index and limit in the API Reference and Database Schema is meant to be verifiable against
convex/.
Getting help
Open an issue for bugs and feature requests, and check the existing issues first. For how the pieces fit together, start with Project Structure and the Architecture section.