because I got bored of customising my CV for every job
TypeScript 92%
MDX 4%
Shell <1%
CSS <1%
Handlebars <1%
Dockerfile <1%
JavaScript <1%
<1%
HTML <1%

README.md

cv-generator #

AI-assisted CV builder. Parses incoming CVs (PDF, GDPR export, manual entry), customises them per vacancy, and renders to HTML/PDF via a Playwright worker.

  • Issue tracker: Linear CVG (workspace riotbyte, initiative Ship CV Generator)
  • Remote: git@tangled.sh:mokkenstorm.dev/cv-manager (GitHub mirror at riotbyte-com/cv-generator)

Repo layout #

This is a bare repo with worktrees, not a normal clone.

cv-generator/
├── .bare/            bare repo (git objects, refs, config)
├── .git              file pointing at .bare
├── .worktrees/       feature branch worktrees (one per CVG-N)
│   └── cvg-<N>-feature-name/      branch: feature/cvg-<N>-feature-name
└── main/             primary worktree, branch: main

All code lives inside worktrees. Don't try to check out files in the container directory itself.

git worktree add .worktrees/cvg-<N>-feature-name -b feature/cvg-<N>-feature-name main
git worktree list
git worktree remove .worktrees/cvg-<N>-feature-name

Monorepo #

pnpm workspaces orchestrated by lerna.

Apps #

Path Role
apps/api Thin GraphQL shell: resolvers, types, inputs, DataLoaders, REST sidecars. NestJS 11 + Apollo Server 5 + Express 5.
apps/worker NestJS standalone process. Consumes the project-q queue. No HTTP, no GraphQL. Renders PDFs via Playwright.
apps/client React + React Query + GraphQL codegen. Vite.
apps/docs Documentation site (Vite).

Packages #

Path Role
packages/core Thick NestJS module library: domain entities, services, Prisma, mail, database. Schema lives in packages/core/prisma/.
packages/auth Guards (JwtAuthGuard, VerifiedScopeGuard), passport strategies, AuthModule. Imported by apps/api. (Auth domain entities live in @cv/core/auth.)
packages/handlers Project-q queue handlers (worker-side message → domain dispatch).
packages/ai-parser LLM-driven CV parsing. Provider-agnostic.
packages/ai-provider Self-registering AI provider registry (OpenAI, Anthropic, Ollama, ...).
packages/cv-renderer Handlebars → HTML. HTML only, no Playwright - the worker owns rendering to PDF.
packages/handlebars Shared Handlebars helpers/partials.
packages/file-storage File backend abstraction (local, S3-compatible).
packages/file-upload Upload pipeline primitives.
packages/mail Transactional mail.
packages/routing Shared React Router config / link helpers.
packages/ui Shared client components (ESM).
packages/utils compact(), raise(), misc.
packages/biome-config Shared Biome config.
packages/tsconfig Shared tsconfig bases.

Pipeline #

input (PDF / GDPR ZIP / manual)
  → ai-parser
  → customisation pass
  → cv-renderer (Handlebars → HTML)
  → worker (Playwright → PDF)

Local dev #

pnpm install
pnpm prisma:generate
pnpm db:setup        # prisma:deploy + seed
pnpm codegen         # prisma:generate + GraphQL codegen + docs MDX (needs `apps/api/schema.gql` on disk)
pnpm dev             # all apps in parallel

Local optional LLM:

pnpm llama:start     # boot Ollama
pnpm llama:stop

.env* files are permission-blocked by tool settings; consult git show HEAD:.env.example for the template.

CI / deploy #

File Purpose
.github/workflows/ci.yml Lint, typecheck, build, audit on every push.
.github/workflows/release.yml Tag push → build & push server/worker images to GHCR via ci/build-images.sh.
.github/workflows/deploy.yml Manual dispatch on a tag → kubectl set image on the production cluster.
.github/workflows/deploy-client.yml Tag push → deploy SPA to Cloudflare Pages via ci/deploy-client.sh.
.github/workflows/create-release.yml Tag bookkeeping.
ci/pipeline.ts Local mirror of CI steps (install / prisma / lint / typecheck / build / audit). Run as pnpm tsx ci/pipeline.ts [step].
.docker/ Production Dockerfiles + pruned workspace manifests (copy-manifests.sh).

Cluster manifests (Prometheus, ingress, cron jobs, secrets) live in a separate infra repo, not here.

Conventions #

  • Commits: Conventional Commits, one-line header, scope is the Linear key (e.g. feat(cvg-84): move CV parsing onto worker queue).
  • Branches: feature/cvg-<N>-feature-name matching the Linear issue.
  • TypeScript: strict, exactOptionalPropertyTypes enabled. Use compact() from @cv/utils for Prisma inputs that may contain undefined.
  • Style (TS/JS): functional patterns, guard clauses with braces, ternaries for simple conditionals, no if/else ladders, no for loops, no inline comments (code should be self-documenting). Biome enforces.
  • GraphQL codegen needs a running API (localhost:3000/graphql) for schema introspection.
  • Async jobs run on the project-q queue. The API enqueues, the worker handles. Job status persists to the AsyncJob Prisma model.
  • Zod 4: import from "zod/v4". z.object() is strict by default; use z.looseObject() for passthrough.
  • Prisma 7: import error classes directly, not via the Prisma. namespace (TS 5.9 fails to narrow through the re-export).