Architecture

The shape of the active CareerVector codebase.


Stack at a glance

Layer Choice Why
Front-end framework Svelte 5 Smallest bundle, fastest runtime, smallest per-component scope (good for AI agents to write). Compiler optimizes reactivity.
Meta-framework SvelteKit First-class Cloudflare Workers adapter, file-based routing, server functions, conventional. SPA-shape with optional server bits.
Build Vite (via SvelteKit) Standard, fast, well-supported by SvelteKit.
Backend Cloudflare Workers Same compute primitive, no Pages adapter wrappers. Free tier.
Realtime broker Durable Object per workspace Thin byte relay for live collaboration. Durable writes go through the api worker first.
Private workspace data D1 + Yjs D1/Y.Doc owns the workspace rung: CVL, CL, workspace facts, evaluations, local summaries.
Shared knowledge data CockroachDB Shared role/ad facts, jobcache, work cache, embeddings, matching, evidence, approved aggregate outputs.
Blob storage R2/B2 Typst PDFs, raw scrape snapshots, uploads, and other large immutable blobs.
LLM access Vercel AI SDK in browser (cascade) Client compute = free. BYOK + free-tier-preference + masked keys remain.
Document rendering Typst WASM in browser Client compute = free. WASM cached in Service Worker for repeat visits.
Agent API MCP server as separate Worker Wraps REST as MCP tools. Agents (Claude Code, etc.) connect, drive the tool same as humans.
Tests Playwright (e2e) + Vitest (unit) Same as v1, behavior-test-first discipline.

Top-level directory layout

The repo IS CareerVector by name. Top-level peers are CV's perspectives (ui/mcp/wiki/status/ops/qa…) + libraries + the JobCache peer product + tools + R&D + knowledge.

careervector-cvl/
├── ui/                        ← perspective: human, interactive (SvelteKit)
├── mcp/                       ← perspective: AI agent, MCP protocol
├── relay/cf/                  ← infra: WorkspaceDoc DO (thin byte-relay supporting ui/)
├── relay/deno/                ← infra: Deno Deploy fallback (vendor-resilience)
├── relay/shared/              ← @cv/relay-shared: wire constants + workspace-id parser
├── lib/                       ← CareerVector-internal libraries
│   ├── domain/                ← @cv/domain — LLM helpers, scoring, tailoring, Typst serializer, cascade
│   ├── schemas/               ← @cv/schemas — Zod (Node tree, jobs, settings, format)
│   ├── mutations/             ← @cv/mutations — op catalog + apply/validate
│   ├── workspace-client/      ← @cv/workspace-client — SDK for ui/MCP write paths
│   ├── health/                ← @cv/health — Health/Check/Metric cross-perspective state shape
│   └── importer/              ← @cv/importer — workspace import logic
├── jobcache/                  ← peer product (shared role/ad corpus; rebrand-ready)
├── experiments/
│   └── cvl-engines/           ← active R&D: Node-tree CRDT alternatives (loro, yjs, …)
├── tools/                     ← infra utilities, NOT products
│   ├── quality/               ← Rust gate/ledger/policy core
│   └── typst-compile/         ← Koyeb-hosted Typst PDF service
├── wiki/
│   └── content/               ← architecture, vocabulary, runbooks, studies, this file
├── archive/                   ← retired knowledge (handoffs, superseded plans)
├── migrations/                ← CareerVector D1 SQL migrations
└── scripts/                   ← repo-level utilities

lib/domain is the framework-agnostic core: scoring, conventions, Typst Node serializer (nodeToTypst), the LLM cascade, the tailor pipeline. lib/mutations owns the op catalog (OP_CATALOG_VERSION) that ui, MCP, and the relays all share.

Data layer in detail

Y.Doc structure per workspace

CV/CL content is stored as independent peer trees, one per language. There is no cross-language inheritance; each language tree is complete for that language. See TREE-OF-CHOICES.md for the full design rationale.

D1/Y.Doc owns the private workspace rung. Raw workspace jobs are public { url, attributes } entries. Cockroach owns shared role/ad facts and jobcache. Workspace-facing APIs, UI, browser workers, and MCP expose job terminology and resolved job projections; they never expose direct role/ad access or raw ad_id / role_id values. Hidden shared ids live only in server-side link/outbox/shared-corpus storage.

workspace.doc (Y.Doc — partitioned into sub-docs persisted as workspace_sub_doc rows)
├── settings: Y.Map           // workspace-global settings (kanban, scoring, AI prefs)
├── layout: Y.Map             // column visibility, widths
├── order_state: Y.Map        // column / group / job manual ordering
├── cv_profile: Y.Map<lang, Y.Map>
│   ├── en: Y.Map             // English peer tree; complete and authoritative
│   │   ├── tree: Y.Map       // recursive Node tree (children[] + selection.active[])
│   │   └── format: Y.Map
│   └── de: Y.Map             // German peer tree; independent of en
│       ├── tree: Y.Map
│       └── format: Y.Map
├── cl_profile: Y.Map<lang, Y.Map>    // same per-language peer-tree shape as cv_profile
├── jobs: Y.Map<jobId, Y.Map> // public workspace jobs: { url, attributes }
│   └── {jobId}: Y.Map
│       ├── url
│       └── attributes: Y.Map
│           ├── title, organization, etc.
│           ├── tailored_cv: Y.Map  // full Node-tree snapshot taken at tailor time
│           ├── tailored_cl: Y.Map  // full Node-tree snapshot taken at tailor time
│           ├── evaluations: Y.Map
│           └── origins: Y.Map
└── cloud_keys: Y.Map         // BYOK + chain config

Key properties of the tree shape:

  • tree: Y.Map is the recursive Node shape: every node has children[] (the pool of candidates) and selection.active[] (the ordered subset that renders).
  • Top-level section variants live inside variant-pool wrapper Nodes. The wrapper's selection.active[] picks the rendered member; the parent's selection.active[] decides whether the wrapper renders at all.
  • tailored_cv / tailored_cl are full tree snapshots taken at tailor-time from a single language tree. Each application diverges independently after the snapshot.
  • Each non-English language tree is independent. Per-language cultural differences (section ordering, omitted sections, distinct content) are handled at the tree level, not via per-node language filters.
  • Node IDs are nanoids; Yjs handles convergence.

Persistence

D1 holds:

  • workspace_sub_doc update bytes for private workspace docs (settings, cv-profile, cl-profile, jobs, notes).
  • Server-side link/outbox rows from workspace job IDs to hidden shared ad/role records.
  • Workspace metadata (id, slug, name, created_at).
  • Operational ledgers such as process requests and compute locks.
  • Workspace-local summaries and projection caches.

The relay workers hold no durable Y.Doc authority. They relay update bytes for live peers after the api worker has committed them to D1.

Cockroach holds:

  • role, ad, org shared facts.
  • jobcache frontier/control rows, work-cache rows, and evidence.
  • Embeddings and matching indexes.
  • Approved aggregate outputs and candidate artifacts/indexes the product is allowed to use.

R2 holds:

  • Old snapshots (older than X days)
  • User-exported PDFs (cached if computed server-side, but most exports stay client-side)
  • Large blobs

Sync protocol

Standard Yjs WebSocket protocol via y-protocols. Awareness for presence (replaces the v1 upper-right indicator's data path; the indicator UI is unchanged per principle 13).

Reconnecting clients sync via Yjs vector clocks — only missed ops are sent. No full-state refetch.

Compute placement

Following PRINCIPLES.md #1 (compute economics):

Operation Where Why
Yjs ops application (write) Client CRDT primitive, deterministic, free for us
Yjs ops broadcast Realtime worker Live fanout only
Yjs persistence D1 Private workspace durability
LLM calls (extract, evaluate, tailor) Client (Vercel AI SDK) Free for us; BYOK; cascade
Typst rendering Client (WASM) Free for us; live latency
Typst PDF export Client (WASM, same render pipeline) Free for us; consistent with preview
Workspace creation Server (Worker) Trivial DB write, must be server
Ad/workspace facts via REST Server (Worker, thin) Private workspace writes, plus outbox commands for hidden shared role/ad storage
MCP server Server (Worker, thin) Per-request, lightweight
Image / asset optimization Client / cache Free for us

Workers do as little as possible. Each request well under 10ms CPU.

Routing

SvelteKit file-based, under ui/src/routes/:

ui/src/routes/
├── +layout.svelte                 // shell
├── +page.svelte                   // landing / workspace creator
├── [wsId]/
│   ├── +layout.svelte              // workspace shell, presence indicator
│   ├── +page.svelte                // redirects to /dashboard
│   ├── dashboard/+page.svelte
│   ├── kanban/+page.svelte
│   ├── cvl/+page.svelte
│   └── profile/+page.svelte
└── view/
    └── [wsId]/
        └── +page.svelte             // future read-only mode (CLAUDE.md §2)

API surface

Two kinds of server endpoints:

Read-only views

GET /api/workspaces/:id returns a snapshot of the current Y.Doc state (for fresh client load before WebSocket sync).

Action triggers (idempotent)

POST /api/jobs/:id/tailor — server doesn't tailor (LLM is client-side); this just kicks the right Yjs op into the broker.

The MCP server (separate Worker) wraps these REST endpoints as MCP tools.

Security model

  • Link = identity. No auth. Workspace URL = full access.
  • API keys (cloud_keys) returned MASKED on GET /api/workspaces/:id. Real keys only via GET /api/chain?workspace= which is the execution path used by client LLM calls.
  • Dev-mode env keys: gated behind APP_MODE === 'dev'. Production uses BYOK only.
  • All writes idempotent where possible (agents will retry).

Extension points (for future)

  • Read-only mode (/view/:wsId/*) — same data, all writes disabled globally. Per CLAUDE.md §2.
  • Multi-locale beyond current 30 — convention table in lib/domain/src/conventions.ts extends.
  • Agent activity attribution — Yjs awareness already includes userId; just expose it in the existing presence indicator (don't extend presence UI per principle 13).
  • Native mobile — if ever wanted. Yjs has bindings for Swift/Kotlin via Automerge-style. Long-tail.
Source: wiki/content/architecture/ARCHITECTURE.md