For AI agents: a documentation index is available at /llms.txt. Every page also has a Markdown version at its URL plus .md.

← All projects

personal

Jobbuddy

AI job-application platform - from posting to tailored CV

A Spring Boot (strict hexagonal) + Angular platform that ingests job postings and generates tailored CVs and cover letters with ATS match reports, backed by Postgres full-text and pgvector semantic search.

What it is

A job-application platform that fetches postings through web crawling and RSS/API connectors, keeps a structured career profile, and uses an LLM to generate CVs and cover letters tailored to a specific posting - with ATS match reports and PDF export.

Architecture

The backend is strict hexagonal (ports & adapters): controllers depend only on inbound ports, use cases only on outbound ports, so the AI provider, search engine and persistence are swappable adapters. All AI output is structured JSON assembled server-side into a StructuredDocument model that both the UI and the PDF renderer consume - no raw text blobs.

Highlights

  • Privacy by design - the CareerProfileForAi payload excludes all PII; identity is merged into documents after the AI call
  • All-Postgres search - tsvector full-text for keywords, pgvector for semantic search (embedding-based query vectors)
  • Writing templates per CV section - STAR, XYZ or CAR for experience bullets, plus personas you write yourself
  • Pluggable LLMs - the model provider is an adapter behind a port, swappable without touching use cases
  • Angular standalone components with lazy routes, Firebase JWT auth, Flyway migrations, OpenAPI docs, Docker Compose
README.md - jobbuddy▼

Jobbuddy

I built Jobbuddy for my own job search, because tailoring a CV and cover letter to every single posting by hand gets old fast. It pulls in job postings, keeps a structured career profile, and uses AI to draft a tailored CV, cover letter and ATS (Applicant Tracking System) report for a specific posting, exported as PDFs.

It used to be called AutoApplicant. The Java package (com.autoapplicant) and the default database name still carry that name.

Dashboard

Pipeline Company research notes
Application pipeline Company page with research notes

Screenshots are from a local run with a made-up account, companies and postings, signed in through the Firebase Auth emulator and with no AI key, so nothing AI-generated is shown.

Jobbuddy flow: job posting to a tailored, ATS-ready application

What it does

  • Career profile with experience, education, skills and projects. You can bootstrap it by parsing an existing CV.
  • Job discovery: a crawler over Jobindex, Jobnet, IT-Jobbank, Jobdanmark and a few ATS boards, plus Postgres full-text and pgvector semantic search.
  • LinkedIn connector: personal-use and low-volume, over LinkedIn's public jobs-guest endpoints, driven by LLM-generated keyword plans per user. It runs on its own jittered schedule, separate from the shared crawl (see docs/guides/db.md and app.linkedin.* in application.yml).
  • Document generation: CVs and cover letters tailored to one posting. An optional drafter/reviewer loop critiques and revises each draft before it's assembled.
  • ATS reports on how well a generated document matches the posting.
  • Guardrails against the model making stuff up: anti-fabrication rules (including not conflating tools of the trade), scraped job text treated as untrusted input, and a deterministic fact gate that flags metrics the profile doesn't support. The fact gate is plain code, no model call.
  • Pluggable providers: OpenAI, Gemini, or a local CLI agent (Claude Code / Codex) so generation can run on a flat-fee subscription instead of per-call API billing. Embeddings always use a real API.
  • All AI output is structured JSON, assembled server-side into a StructuredDocument (identity, sections, rendering options), then rendered to PDF with either an ATS-friendly or a designed template.
  • Name, email, phone, photo and links are never sent to the AI provider. Identity is merged into the document after the AI call.
  • Firebase JWT auth, optional LinkedIn integration, and an OpenAPI spec with Swagger UI.

Stack

Layer Technology
Backend Java, Spring Boot, Gradle (multi-module)
Architecture Hexagonal (ports and adapters): domain / port / usecase / adapter
Frontend Angular (standalone components, lazy-loaded routes)
Database PostgreSQL with Flyway migrations
Search Postgres full-text (danish config) + pgvector (text-embedding-3-small)
AI OpenAI / Gemini API, or a local CLI agent (Claude Code / Codex) for generation
Auth Firebase Authentication (JWT), optional LinkedIn OAuth
Docs Springdoc OpenAPI / Swagger UI
Infra Docker Compose (Postgres, backend, frontend)

Architecture

The backend is hexagonal. Anything crossing a boundary goes through a port interface:

adapter/            Spring controllers, JPA adapters, AI client, crawler, PDF renderer
  web/controller/   REST endpoints, depend on port/in interfaces only
  persistence/      JPA entities + adapters implementing port/out
  ai/               OpenAI client implementing AiProviderPort
  crawler/          Job posting crawler
  pdf/              PDF rendering
port/
  in/               Use case interfaces (what the application can do)
  out/              Repository/external service interfaces (what the app needs)
usecase/            Business logic, implements port/in, depends only on port/out
domain/             Plain records/value objects, no framework dependencies

The frontend follows the same idea: core/api (HTTP services), core/models (interfaces mirroring the backend records), features (routed components), shared/components (presentational).

Running it

Full stack in Docker:

cp .env.example .env   # then fill in secrets
docker compose --env-file .env -f infra/docker-compose.yml up --build

Or locally, with only Postgres in Docker:

docker compose --env-file .env -f infra/docker-compose.yml up postgres -d

./gradlew :backend:bootRun                       # backend
cd frontend && npm install && npm run start:local  # frontend
Service URL
Frontend http://localhost:4200
Backend API http://localhost:8080/api/v1
Swagger UI http://localhost:8080/swagger-ui.html

Configuration

You need:

  • OPENAI_API_KEY, with access to gpt-4o and text-embedding-3-small
  • a Firebase service account JSON at .secrets/firebase-service-account.json

Optional: DB_*, LINKEDIN_CLIENT_ID/SECRET, ALLOWED_ORIGINS (all have local defaults).

AI provider and feature toggles (optional, with defaults):

  • GENERATION_AI_PROVIDER: openai (default), gemini, or claude-cli / codex / cli for a local agent. ENRICHMENT_AI_PROVIDER has to stay a real API because it produces embeddings.
  • AI_CLI_COMMAND: optional override of the local-agent command line (prompt piped to stdin). Blank picks a sandboxed default per provider: Claude Code with no tools, no MCP servers and no session transcript, or Codex under a read-only sandbox. An override replaces those flags, so it has to carry its own.
  • AUTO_REVIEW_ENABLED: the reviewer critique/revise pass after generation (default true; each pass is one extra LLM call).
  • FACT_GUARD_ENABLED / FACT_GUARD_MODE: the fact gate on generated metrics (warn by default, or block).
  • LINKEDIN_SCRAPER_ENABLED, LINKEDIN_LOCATIONS: the LinkedIn connector (see app.linkedin.* in application.yml).

Tests

./gradlew :backend:test          # backend: unit, web-layer, ArchUnit and Postgres integration tests
cd frontend && npm run test:ci   # frontend, headless
cd frontend && npm run lint      # lint

Persistence tests run against a real pgvector/pgvector:pg16 container (Testcontainers) migrated by Flyway, so full-text ranking, vector search and the migrations are tested on actual Postgres. They need Docker and get skipped locally without it; CI always runs them. HexagonalArchitectureTest (ArchUnit) enforces the layering above, and a full test run fails if line coverage drops below the JaCoCo floor in backend/build.gradle.kts (only ever raise it).

Docs

The rest lives in docs/ (start at the index): specs/, architecture/, guides/ (setup, commands, testing, db), product/ (strategy, features, notes on the Danish job market) and archive/ for old material. Agent guidance is in CLAUDE.md.

command.exeesc

↑↓ select · Tab complete · Enter run · Esc close

doom.exe

WASD move · ←→ turn · Space fire · E use · Shift run · Esc menu · click to capture mouse · licences