verified against source, not aspirational
Technology Radar
Adopt, Trial, Assess, Hold — what this app runs on today, and why. Every dependency entry is checked in CI against the real package.json/Cargo.toml files on every push; an entry naming something removed from the stack fails the build instead of quietly going stale.
Renderer & UI
AdoptIn production today — the default choice for new work in this area.
Tauri 2
AdoptOS-native WebView desktop shell — not Electron.
Rust backend plus the OS's own WebView (WebView2 on Windows, WebKit on macOS/Linux) instead of bundling Chromium: installers land in the tens of MB rather than 150+ MB, and the renderer only reaches Rust through commands explicitly allowed by Tauri's capability manifest. The trade-off — WebKit on macOS doesn't always render identically to WebView2 — is handled with a cross-platform CSS pass before each release.
React 19
AdoptConcurrent rendering, Actions, first-class TanStack support.
The renderer is React 19.2 throughout apps/desktop and apps/landing — Actions/useActionState for async transitions, ref as a plain prop, and the concurrent-safe integration TanStack Query and Zustand both depend on.
TanStack Query 5
AdoptThe only sanctioned way for a component to reach IPC.
Every server-state read/write goes through a service hook in renderer/services/ — no useState + useEffect fetching, no direct window.api access from features/routes/components. Query keys are centralized so cache invalidation after a mutation is deliberate, not guessed at.
TanStack Router
AdoptFile-based routing with typed route + search params.
Chosen for compile-time-checked routes and search-param types over a stringly-typed router API — the desktop app has 11+ feature routes, and a renamed route param fails at build time instead of at runtime.
Zustand 5
AdoptMinimal client-only state — persisted prefs, transient session.
Used over Redux because the client-state slices (persisted preferences, the transient generation session) are simple enough that Redux's action/reducer boilerplate bought nothing. Zustand is a plain hook with no provider tree, and it's React 19 concurrent-safe.
Tailwind CSS 4
AdoptCSS-first @theme config — the design-token backbone.
packages/ui defines every design token (--color-brand, --color-surface-elevated, …) as a CSS custom property consumed via Tailwind 4's @theme. ESLint then bans a raw hex color in any className, so a color can't drift from the token file without the linter catching it.
Motion (motion/react)
AdoptAnimation library, wrapped behind named transition tokens.
packages/ui/src/lib/motion.ts exposes named presets (transition.fast/.spring/.modal/…) over the raw library; ESLint blocks an inline { duration, ease } object anywhere in feature code, so every animation in the app traces back to one small token file.
Hand-rolled state machine (lib/machine.ts)
Adopt~80-line machine + a useMachine hook for any 3+-state flow.
Any flow with 3+ states (onboarding, streaming generation) gets a state machine. Named states replace boolean tangles (isLoading && isDone) and make an impossible state impossible to represent, without pulling in a general-purpose library — see XState, held, for why not that one.
WCAG 2.2 AA
AdoptThe non-negotiable accessibility floor for every route.
Enforced via eslint-plugin-jsx-a11y, @axe-core/playwright, and apps/landing's own check:a11y script; /accessibility publishes the conformance statement. Colour is never the sole signal for state — including on this very page's ring encoding — and every interactive element ships a visible :focus-visible ring.
HoldDeliberately not used here. Proceed with caution; the reasoning is in the entry.
XState
HoldConsidered for the same job as our micro state machine — held.
The flows in this app top out at a handful of linear states, so XState's parallel states, history, and guards buy nothing today, at the cost of bundle weight and its own config DSL. Kept as an explicit option if a flow ever genuinely needs what it offers — this entry exists so that reasoning stays visible instead of getting re-litigated.
WebGL / shader-driven landing experiences
HoldTwo hand-built WebGL landing pieces were built, then shelved.
TERMINAL VELOCITY (a scroll-driven CG retelling of the job hunt) and RIPBOOK (a notebook-styled concept) were each built across real milestones and then deliberately abandoned before their visual approval gates passed — the landing site returned to a plain static/Next export. The dormant webgl-author/shader-engineer/webgl-reviewer agent trio stays dormant; a returning WebGL surface would route back to them, not the general frontend author.
Rust Core & Data
AdoptIn production today — the default choice for new work in this area.
SQLite via rusqlite (bundled)
AdoptThe whole local-first store — no external DB process.
rusqlite's bundled feature ships SQLite inside the binary, so there's no version mismatch or separate database process to install. Seven independent databases (documents, conversations, ai_generations, job_preferences, contact_profile, jobs, pipeline_cache) keep faults isolated — a corrupt conversations.db doesn't touch documents.db.
Tokio
AdoptAsync runtime for board scraping and background jobs.
Board scrapers run concurrently via tokio::spawn, each holding a CancellationToken so a scrape can be stopped mid-run; long operations are tracked by a SQLite-backed job tracker with retry rather than blocking command handling.
keyring-core (OS-native credential storage)
AdoptAPI keys and board passwords never touch the renderer.
Credentials live in the OS keychain (Credential Manager/DPAPI on Windows, Keychain on macOS, libsecret on Linux) via keyring-core's platform adapters. The renderer calls credential commands over IPC and never handles a raw secret — a renderer XSS can't reach them because they live outside the web context entirely.
chromiumoxide (headless Chromium automation)
AdoptDrives a real browser for boards that block plain HTTP.
LinkedIn is scraped over plain HTTP, and the walled boards (Indeed, Glassdoor, StepStone, Xing, Workday) mostly go through the Adzuna/JSearch aggregator — chromiumoxide backs the specific boards and login flows that genuinely need a real, scriptable browser rather than a bare HTTP client.
reqwest
AdoptThe one HTTP client every scraper and AI call goes through.
Centralized in net/http.rs so every outbound call — scraping, AI providers, geocoding — shares one client, one timeout policy, and one place to audit against the network-egress boundary.
Zod
AdoptSchema-first validation at every IPC and form boundary.
IPC payloads and form data are validated with Zod at the boundary (IPC receive, form submit); inside the app the inferred TypeScript types are trusted, so component logic isn't full of defensive if (!data?.id) checks.
Ollama (local + cloud)
AdoptThe offline-first AI provider — a local model, not just an API key.
The one AI provider that needs no API key and no network call at all: a locally-run model the app talks to over loopback HTTP, with an optional Ollama Cloud mode alongside it. It's the concrete reason 'no API key yet' doesn't mean 'no AI features yet' for a local-first app.
Live model listing, no hardcoded defaults
AdoptEvery provider model list is fetched live — never a curated array.
Four stale-model defects shipped in one session from hand-curated model arrays (a retired embedding model left as the default, a shut-down Gemini preview, its equally-dead list neighbours). ADR-0022 deleted every hardcoded array; a provider's own /models endpoint is now the only source, and onboarding pre-selects nothing.
HoldDeliberately not used here. Proceed with caution; the reasoning is in the entry.
A dedicated vector database
HoldConsidered for posting-embedding search — held.
Posting embeddings are stored in SQLite alongside the documents and searched with an in-memory cosine pass, not in Pinecone/pgvector/a standalone vector engine — the corpus a single local user accumulates is small enough that the extra dependency and the extra moving part it would add buy nothing measurable today.
Nominatim
HoldRetired as the geocoding fallback — its usage policy forbids autocomplete.
Location autocomplete answers offline from a bundled GeoNames index for virtually every query; only a genuine miss falls through to Photon (OpenStreetMap-backed) as the network fallback. Nominatim filled that fallback role first and was retired because its usage policy explicitly forbids autocomplete-style querying.
Documents & Export
AdoptIn production today — the default choice for new work in this area.
Typst (typst / typst-pdf / typst-layout / typst-svg)
AdoptOne pure-Rust engine renders every résumé and cover-letter PDF.
A single Typst engine backs both documents so résumé and cover-letter output share one layout system instead of two. The whole family is exact-pinned to =0.15.1 in lockstep — a solo version bump anywhere in it is a red flag, not routine maintenance.
docx-rs
AdoptNative DOCX generation for the résumé's second export format.
Renders the canonical document model straight to a real two-column DOCX table with native ATS-mode support, guarded by golden invariants so parity between the DOCX and PDF paths doesn't silently drift.
lopdf
AdoptLow-level PDF manipulation — including inline annotation dicts.
Used where PDF structure needs direct manipulation rather than pure rendering; inline (non-referenced) /Annots dictionaries needed custom parsing since lopdf's own annotation handling assumes the referenced form.
pdf-extract
AdoptText extraction for imported PDF résumés.
Backs step one of the document-import pipeline (format detection → text extraction → SQLite storage → chunking → embedding) for PDF specifically; DOCX and images go through their own dedicated parser/OCR paths.
image (Rust crate)
AdoptRaster handling for OCR input and export assets.
Exact-pinned alongside the Typst family (=0.25.10) since Typst's own SVG/PDF export path depends on it — kept in lockstep rather than left to float independently.
Dual-engine golden-parity migration
AdoptThe legacy renderer stays compiled as the parity reference.
layout_pdf and model_docx are on by default now that the canonical layout engine has snapshot parity with the legacy line-based renderer, but the legacy path stays compiled — it's still the parity reference, and it still renders cover letters — so --no-default-features can fall back to it if the canonical path ever regresses.
Build, Ship & Trust
AdoptIn production today — the default choice for new work in this area.
Vite 8
AdoptDev server + build tool for the desktop renderer, landing, and the extension.
Every buildable frontend workspace (apps/desktop, apps/landing, apps/extension, packages/ui) builds on Vite; Vitest is Vite-native, so the same config/plugin surface backs both dev and test.
Vitest 4
AdoptThe one test runner, in every workspace.
A root vitest workspace config aggregates every package + app project — plus a dedicated node-env project for build/release scripts — into one coverage report, so `pnpm test` is a single command regardless of which package changed.
Playwright
AdoptAxe-driven accessibility checks, and the desktop app's E2E suite.
Backs apps/landing's check:a11y (@axe-core/playwright) and apps/desktop's Playwright E2E suite (test:e2e) — the same tool covers both a static export and a live Tauri window.
Turborepo
AdoptIncremental builds across an 8-workspace monorepo.
Tracks file hashes per package, so an unchanged packages/shared build is skipped entirely when only apps/desktop changed — the dependency graph is why CI build time doesn't scale linearly with workspace count.
semantic-release
AdoptCommit-driven, manually-triggered versioning — never automatic.
Driven by Conventional Commits (feat → minor, fix/perf → patch, BREAKING CHANGE → minor pre-1.0), but nothing runs on push/merge to main — a release is a deliberate Actions dispatch, never an automatic side effect of merging.
Husky + lint-staged pre-commit gate
AdoptEvery commit is linted/formatted before it lands, not after.
Pre-commit runs eslint --fix on staged TypeScript and Prettier on the rest; commitlint checks the message. Pre-push runs the full gate (typecheck, lint, cargo check/test/clippy, formatting) so main never carries a known lint or type error.
ESLint + ast-grep architecture guardrails
AdoptThe rules in AGENTS.md are enforced, not just written down.
Package-boundary imports, the ports-and-adapters window.api ban, hardcoded hex colors, inline transition objects, raw <button>/<select>/<textarea> — every one of those rules is an ESLint error or an ast-grep scan rule, not a convention someone has to remember and a reviewer has to catch by eye.
Sentry crash reporting
AdoptDesktop-only, default ON, consent-gated, whole-event redacted.
Adding remote crash reporting reversed a published no-telemetry promise, so the decision to do it is recorded explicitly: default on, but nothing transmits until the first-run wizard has actually shown the consent screen, every outgoing event is redacted, the DSN is baked only into signed release builds, and both the browser extension and the landing site are excluded.
CodeRabbit
AdoptThe always-on AI PR reviewer — advisory only, never blocking.
Free and unlimited on public repos, and it overlapped three separate advisory lanes (a reviewdog ESLint/Clippy pass, a Dangerfile, an actionlint lane) closely enough that all three were retired in its favour. It's configured to never approve or block on its own — the required check stays CI, plus an on-demand deep-dive review for anything that needs one.
Next.js (static export)
Adoptapps/landing is plain HTML/JS at runtime — no server, ever.
output: 'export' — no middleware, no Server Actions, no ISR, no dynamic route handlers, no headers()/cookies(). A permanent check:parity gate diffs the built out/ against the legacy static site so a route can never silently change shape.
TrialShipping, with a specific caveat or a partial rollout worth knowing about.
TypeScript
Trial7.x (native compiler) everywhere except apps/landing, pinned to 6.x.
apps/desktop and every packages/* workspace run TypeScript 7's native compiler; apps/landing stays pinned to 6.0.3 because Next 16's build-time verifyTypeScriptSetup doesn't recognise TS 7's native-compiler package layout and crashes the build worker. Trial, not Adopt, until Next supports it and the pin can drop repo-wide.
AssessNot in the codebase yet — worth understanding and watching before committing.
React Compiler
AssessGA since React 19, not yet wired into any build here.
No babel-plugin-react-compiler or eslint-plugin-react-compiler dependency exists anywhere in the monorepo today — manual memoization discipline is still how the renderer avoids re-render cost. Worth a real trial once it's had more mileage against a render-heavy, streaming-text-updating UI like this one; not adopted yet because nobody has actually run it here.
HoldDeliberately not used here. Proceed with caution; the reasoning is in the entry.
Codecov + SonarCloud
HoldDropped in favour of the zero-external-SaaS CI default.
The CI program's stated default is Actions-native, zero external SaaS advisory tooling; both were dropped for exactly that reason before CodeRabbit was even evaluated. CodeRabbit's own free-tier scan (ESLint, Clippy, Semgrep, secret-scan) made a paid hosted coverage/quality dashboard even less necessary once it was adopted.