Back to Resume
PrivateDecember 2025· 5 weeks

MyCVPath, AI-Native CV Intelligence Platform

mycvpath: An AI-Native CV Intelligence Platform

RoleFounder and sole engineer

mycvpath is a CV tailoring platform I designed, built, and run myself. A user brings a CV and a job posting, and a six-agent LLM pipeline parses both, analyzes the gap between them, rewrites the CV against that specific job, and scores the result, then a separate rendering service turns the output into an ATS-safe PDF or Word document. The same pipeline is exposed as an MCP server, so an external agent such as Claude Desktop, Cursor, or a custom tool-calling agent can drive the whole flow with its own model, or ask mycvpath to run it. It replaces hours of manual CV editing per job application with a few minutes of grounded, automated rewriting, and it proves the same pipeline works both as a consumer product and as agent infrastructure.

The problem

Anyone applying to more than one job faces the same chore. Each posting wants a CV that speaks to its specific requirements, not a generic one, so the honest version of applying to ten jobs is rewriting the CV ten times. Most people do not do that, they send one CV everywhere and hope, or spend an evening per application manually re-ordering bullet points. A generic AI chatbot does not fix this well. Asked to tailor a CV, it paraphrases freely, invents details that were never in the source CV, and hands back prose instead of a document a person can submit or an ATS can parse. It has no memory of what should be preserved versus rewritten, and no scoring step to say whether the result is actually a better match. What was needed was a pipeline, not a prompt, parse the CV and the job posting into structured data, find the real gaps, rewrite the CV against those gaps without inventing facts, score the result, then render it as a real ATS-safe document across multiple LLM providers rather than one vendor.

Constraints

  • ·Solo engineer, one person owning product, backend, AI orchestration, PDF rendering, deployment, and billing
  • ·BYOK-first cost model, the platform cannot absorb everyone's LLM bill, so most usage runs on a user's own provider key
  • ·Multi-provider LLM support required from day one across OpenAI, Anthropic, Gemini, Ollama, OpenRouter, and a self-hosted gateway
  • ·ATS-safe output only, no multi-column layouts, no text boxes over images, nothing that breaks automated resume parsing
  • ·Self-hosted infrastructure on a self-managed Pi behind Docker Compose, not a managed cloud platform
  • ·Two languages from the start, English and Turkish, across the app and public content
  • ·Internal-only MCP surface, trusted and invited use, not a public API, which shaped every auth decision

Architecture

Five independently deployable services share one PostgreSQL instance. A request enters through the web app or the MCP server, both hand off to the same Go orchestrator running the same six-agent pipeline, and a governance layer (quota, spend caps, BYOK key resolution) sits in front of every agent call. Telemetry and the admin dashboard run in the background, fed by fire-and-forget events from the services on the live path.

MyCVPath, AI-Native CV Intelligence Platform architecture diagram
  • Next.js 16 / React 19 app, the BFF and request boundary, CSRF middleware, guest and user identity
  • Go-LLMs orchestrator, the six-agent pipeline (cv-parser, cv-validator, job-parser, job-analyzer, cv-tailor, comparative-scorer) behind one multi-provider interface
  • CV-Manipulation, a Python/Flask service rendering the tailored CV and cover letter to PDF or DOCX with ReportLab, plus OCR ingestion for uploaded files
  • Analytics-Monitoring, a Rust/Axum/Tokio telemetry sink ingesting page views, LLM logs, and errors fire-and-forget
  • Admin Service, a Node/Express read-only dashboard over the analytics schema
  • PostgreSQL 16, one instance, two schemas (public for business logic, analytics_monitoring for telemetry), 22-plus sequential migrations
  • MCP server mounted on the Next.js app (package-request, submit-result, run-agent, get-pdf, get-word), bearer-token auth only

The live path runs left to right, caller, Next.js app, Go orchestrator, Python renderer, document back to caller. Postgres is shared by the app and the orchestrator. Telemetry and admin run as background, fire-and-forget consumers of the live path, not on it.

How one request flows

  1. 01Import: the user pastes CV text or uploads a file (PDF, DOCX, or an image). A file is OCR-extracted in the same call, then cv-parser turns it into a structured Master CV of personal info plus ordered sections and entries.
  2. 02Validate: cv-validator checks the parsed CV for gaps and fills what it reasonably can, so the rest of the pipeline works from a complete structure.
  3. 03Job intake: the user pastes the job posting, job-parser extracts structured requirements, then job-analyzer compares them against the Master CV and produces a gap analysis rather than a paraphrase of the posting.
  4. 04Tailor: cv-tailor rewrites the CV against that specific job and analysis, choosing sections, ordering, and phrasing, constrained by a JSON schema injected into its prompt so the output is always structured.
  5. 05Score: comparative-scorer compares the tailored CV against the job and produces a match score from structured comparison, not narrative guessing.
  6. 06Render: cv-manipulation turns the tailored CV and cover letter into a PDF or DOCX with ReportLab, a legacy fixed layout or a dynamic layout following the tailor step's section order, always ATS-parseable.
  7. 07Deliver and log: the document streams back to the caller, browser or an MCP client calling get-pdf, while every step's LLM request and any error is POSTed fire-and-forget to the Rust telemetry sink.

Engineering decisions

Schema-injected prompts instead of free-form JSON

What. Early agent prompts asked the model to just return JSON, and the code regex-matched the response out of whatever prose or markdown wrapped it. The fix injects the exact JSON schema into the prompt and validates the output against it before the next agent runs.

Why. Providers did not agree on how to wrap that JSON, so parsing broke on a meaningful share of otherwise-correct responses. A schema mismatch now triggers a provider retry instead of quietly passing a malformed object down the pipeline.

Tradeoff. More prompt-design work per agent than a loose instruction to just return JSON, in exchange for every agent's contract being explicit and testable instead of implicit.

Five languages from the first commit, not a monolith split later

What. Next.js for the user-facing app, Go for LLM orchestration, Python for PDF rendering, Rust for telemetry, Node for the admin surface, all designed as separate services before any of them had users.

Why. Each language matches the job it does, Go's concurrency model suits many parallel LLM calls with retry and failover, ReportLab in Python has no real match elsewhere for deterministic document layout, and Rust with Tokio keeps telemetry writes from ever touching user-facing latency.

Tradeoff. A monolith would have been faster to stand up and easier for one person to operate day to day. More deploy and health-check surface was accepted, one Docker Compose stack, five Dockerfiles, migration-gated startup, in exchange for scaling or replacing the PDF renderer without touching AI orchestration.

Two ways into the same pipeline, mycvpath's model or the caller's

What. The MCP server offers two modes on the same six-agent pipeline. Mode B (run-agent) runs the request through mycvpath's own model. Mode A (package-request, then submit-result) assembles the exact prompt and schema, the caller's own agent runs the inference, and mycvpath validates the result before continuing.

Why. An agent that already has a model running should not have to pay mycvpath's token bill twice, so Mode A spends zero LLM tokens on mycvpath's side and stores no provider key for that call.

Tradeoff. Mode A only covers a single LLM turn, agents with tool-calling loops are rejected outright, since a multi-turn tool loop cannot be captured and handed back in one request-response pair. That limit was kept rather than building a stateful multi-turn bridge for an internal-only surface.

Personal access tokens over OAuth for MCP auth

What. MCP originally accepted only the browser's short-lived session JWT as a bearer token, unusable for a long-running agent since it expires in an hour. It now also accepts a long-lived personal access token minted from Settings with a user-chosen expiry.

Why. OAuth 2.1 with Dynamic Client Registration is the MCP spec's own answer and the better end state, but it needs a full authorization server, register, authorize, and token endpoints, PKCE, a consent screen, refresh rotation. That is weeks of work against roughly a day for personal access tokens, and the server is documented as internal, trusted use only.

Tradeoff. OAuth was deferred, not skipped. The access-token table is designed to carry over unchanged the day OAuth is worth building, so deferring did not close off the better path.

Hashing a 256-bit token with SHA-256, not bcrypt

What. Access tokens are stored as a SHA-256 hash with constant-time comparison, looked up by a non-secret 12-character prefix rather than the hash itself.

Why. The token is 32 bytes of CSPRNG output, not a password a person chose, so there is no low-entropy secret for a slow key-derivation function to protect, and bcrypt would only add latency to every authenticated MCP request.

Tradeoff. Minting a token is deliberately restricted to an active session, never to another token, so a leaked token can never mint itself a replacement, which is what actually makes revocation effective.

Decrypt a BYOK key once per request, not once per agent call

What. A user's own provider key is stored encrypted with AES-256-GCM and decrypted once at the start of a request into an in-process value, then carried through however many of the six agents that request touches.

Why. Decrypting fresh at every agent call is the more cautious-looking pattern, but it added measurable latency across a six-agent pipeline for no additional safety, the key already lives only in process memory for the life of that one request either way.

Tradeoff. Decrypt-once trades a marginally larger in-memory blast radius within a single request for meaningfully lower latency, and the key still never touches a log or gets serialized.

Offline-first state so a refresh never loses an in-progress run

What. Workflow pages write to React Context first, sync to LocalStorage after every agent response, and sync to Postgres both after each pipeline step and on browser navigation. Guests get the full workflow with no account, then an atomic migration moves everything to a real account on registration.

Why. Each pipeline step can take real time, and a browser refresh mid-run used to mean starting over.

Tradeoff. The cost showed up later as a documented class of stale-closure bugs, handlers reading from component state that had not caught up with the debounced sync. The fix, reading straight from the storage layer at the exact moment of a side effect, is a per-callsite pattern rather than one systemic fix, and is still being rolled out across every handler that needs it.

Soft delete split by table, not a blanket policy

What. Tables with statistical value, CVs, job applications, users, are soft-deleted, deleted_at is stamped and the row stays. Singleton tables carrying a UNIQUE(user_id) constraint, sessions, builder state, preferences, are hard-deleted instead.

Why. A soft-deleted singleton row would block that same user's very next write, since the unique constraint still counts it, and a deleted session or preference row has no stats value worth keeping.

Tradeoff. A single blanket policy would have been simpler to reason about, but it would have either lost admin-facing history or blocked re-registration on the singleton tables. The split costs one more rule to remember per table.

What changed along the way

  • MCP auth started as the browser's own session JWT, unusable for a long-running agent since it expires in an hour. Personal access tokens replaced it as the primary path, the JWT path stayed for compatibility.
  • Early agent prompts were free-form and needed regex extraction of JSON from the model's prose. That broke often enough to force the move to schema-injected prompts validated before the next agent runs.
  • Workflow state started as plain React state and lost all in-progress work on a browser refresh. It became a three-layer system, Context, LocalStorage, Postgres, with sync firing after every agent response and on navigation.
  • sanitizeCV, which strips blank entries, originally ran on every edit commit and deleted a freshly added blank entry in the same tick it was created. It now runs only at the persistence boundary, save and export, never on the live edit path.

Security and reliability

  • ·One identity layer for both surfaces, a bearer is either an mcp_ access token (database lookup, constant-time hash compare) or a legacy session JWT, both resolve to a bare user id, neither ever carries or grants access to a BYOK provider key
  • ·CSRF enforced on every mutation, any non-GET /api/* request without a valid CSRF header is rejected with a 403 before its handler runs, enforced once in middleware rather than per route
  • ·BYOK provider keys encrypted at rest with AES-256-GCM, decrypted only in process memory for the life of one request, never logged, never serialized
  • ·Rate limits and spend caps checked before the call, hourly and daily per-user per-agent limits and monthly spend caps checked before an agent runs, a platform-wide free mode can suspend billing while still tracking cost
  • ·Migration-gated startup, migrations run as a one-shot Docker container gating every application service, a failed migration fails the whole stack
  • ·Fire-and-forget telemetry, page views, LLM request logs, and errors POSTed over Tokio's async spawn, so a slow or failed write in the Rust sink never adds latency to the request it is logging
  • ·Revocation that actually revokes, token creation requires an active session never another token, so a leaked personal access token cannot mint itself a replacement

Metrics

6
agents in the tailoring pipeline: parse, validate, parse job, analyze, tailor, score
measured
5
independently deployable services sharing one Postgres instance
measured
7+
LLM providers behind one orchestrator interface
measured

What shipped

  • Five services in Go, Python, Rust, Node, and Next.js, first commit to live deployment in ten days, 10 to 19 December 2025. The six-agent pipeline, the PDF renderer, and telemetry all worked on that first production deploy.
  • The six agents run in order: parse the CV, validate it, parse the job, analyse it, tailor the CV, then score the result. Work that took two to four hours by hand now needs under five minutes of attention.
  • Every agent returns output against an injected schema instead of free-form text. Malformed model output stopped breaking the pipeline once the schema-driven version shipped.
  • Requests fail over across OpenAI, Anthropic, Gemini, and OpenRouter, so one provider going down does not stop the product. Users can bring their own encrypted key and run without platform billing in the way.
  • A Rust service takes telemetry on a fire-and-forget Tokio handler, under a millisecond per event, so recording an event never slows the generation a user is waiting on. Cost and behaviour analytics land in the admin dashboard live.
  • A guest who registers keeps everything. The migration moves state across more than ten tables in one transaction, and where a singleton row conflicts, the more recent state wins.
  • Seventeen migrations run from a one-shot container that must finish before any service starts. Each one is self-contained and safe to re-run, so a half-applied schema never reaches production.
  • Bring-your-own-key credentials are sealed with AES-256-GCM, each with its own nonce and authentication tag. A key is decrypted only inside the Go process for the length of one request, so the platform operator cannot read it.

Lessons learned

  • ·Schema-injected prompts, not free-form JSON, are what make a six-agent chain reliable, one malformed hop breaks everything downstream of it.
  • ·Separating identity tokens from provider-key custody is a rule worth writing down, not something to leave implicit, because the two have very different blast radii if either leaks.
  • ·Deferring OAuth in favor of personal access tokens was the right call for an internal, trusted surface, and it only stayed the right call because the token table was designed to carry over unchanged once OAuth becomes necessary.
  • ·Fire-and-forget telemetry keeps the user-facing path fast, but anything logged that way can never be something the product actually depends on, since the write is allowed to fail silently.
  • ·A single soft-delete policy sounds simpler than a split one, but the split, stats tables soft, singleton tables hard, is what actually avoids both lost history and blocked re-registration.

Tech stack

AI & retrieval
Go orchestrator behind one provider interface (OpenAI, Anthropic, Gemini/Vertex AI, Ollama, OpenRouter, a self-hosted OmniRoute gateway, and a mock provider for tests)Six-agent pipeline: cv-parser, cv-validator, job-parser, job-analyzer, cv-tailor, comparative-scorerSchema-injected structured output with prompt-level JSON schema validationMCP package mode for handing a single LLM turn to an external caller's own model
Backend
Next.js 16 / React 19 (App Router) as the BFF and orchestration layerGo 1.24 orchestrator (go-llms)Python 3.13 / Flask document renderer (ReportLab, two rendering engines)Rust / Axum / Tokio telemetry sinkNode.js / Express admin dashboardMCP server (@modelcontextprotocol/sdk, mcp-handler) exposing package-request, submit-result, run-agent, get-pdf, get-word
Data & state
PostgreSQL 16, one instance, two schemas (public, analytics_monitoring)22+ sequential, idempotent migrations, gated Docker startupJSONB provider chains and immutable per-job CV/job/analysis snapshotsOffline-first client state, React Context, LocalStorage, server sync, dual-trigger on agent response and navigation
Ops & quality
Docker Compose, blue-green deploy on a self-hosted PiHealth-gated service startup (migrations as a one-shot gate)CSRF middleware on every mutating API routeAES-256-GCM BYOK key vault, SHA-256 personal access tokensEN/TR i18n across the app and public content