3. Strategic Design — Frontend Bounded Contexts
The nine frontend contexts mirroring the backend 1:1, and the view-vs-context dichotomy. Part of the Frontend Architecture reference.
Read this if you need to know which context owns a given hook, component, or mutation — or why a user-facing page is not itself a context.
A bounded context is a self-contained slice of the domain with its own model and vocabulary; the backend defines the nine this app mirrors in Strategic Design — Bounded Contexts. This page does not redefine them — it shows how each maps to a folder under apps/web/src/contexts/ and how views compose them.
The frontend's contexts are a conformist projection of the backend's contexts (in the DDD sense): the frontend consumes the backend's ubiquitous language as-is and does not push back against the shape. There is one frontend context folder per backend context — Discovery, Enrichment, Profile, Scoring, Materials, Apply, Pipeline Orchestration, Operations, and Contact & Outreach. Views (Dashboard, Jobs, Artifacts, Apply Review, Runs, Pipelines, Discovery, Contacts, Debug) are not contexts; they are composers that live under views/ (§3.10, §11).
3.1 Frontend Context Map
The diagram reads top-down:
- Routes translate URL state into the inputs that views and hooks consume.
- View composers (
views/dashboard/,views/jobs/,views/artifacts/) are the only place that imports from multiple contexts. They own layout, not data dependencies. - Frontend bounded contexts (
contexts/<name>/) are the canonical surface for each backend context; they own their own hooks, components, mutations, and (for Operations) read queries. - Operations is the read-side kernel: the query-key registry, the projection-typed read hooks, the SSE subscription, the invalidation router. Every other context depends on it for read access; no other context owns read queries.
- Shared kernel holds primitives, layout, providers, stores, formatting helpers, and the frontend ports (§6) that abstract the API client and event stream.
3.2 Job Discovery (Frontend)
Backend mirror: Job Discovery (backend domain model §3.1, §4.1, §5.1).
Purpose: Surface the user-facing affordances over the Job aggregate and the discovery-source registry — deleting (soft-delete tombstone), hiding, restoring, and permanently deleting jobs from any view; manually importing a job by URL; and administering discovery sources, schedules, quarantine, the manual-capture queue, and discovery feedback.
Ubiquitous language (matches backend):
- Job — the
Jobaggregate root identified by(TenantId, JobId). - JobId — the system-generated stable identifier (per backend domain model §3.1 / §4.1).
- PostingUrl — the original source URL.
- Source — the board / employer site.
- Employer — the hiring company (distinct from
Source).
Responsibilities:
- Wrap the discovery-context write-side endpoints in mutation hooks:
useDeleteJobMutation,useDeleteJobsBulkMutation,usePermanentlyDeleteJobsBulkMutation,useHideJobsBulkMutation,useUnhideJobsBulkMutation,useRestoreJobMutation,useRestoreJobsBulkMutation, anduseImportJobMutation(import-by-URL; a stub that throwsNotImplementedErroruntil the backend endpoint lands). - Own discovery-source administration:
useDiscoverySettingsQuery/useUpdateDiscoverySettingsMutation, and the product-control mutations (useUpsertDiscoverySourceMutation,usePatchDiscoverySourceStateMutation, promote/reject source-locator candidate, quarantine decision, manual-capture import/dismiss, discovery feedback, role-match feedback). - Own the discovery UI panels
<DiscoveryProductControls>and<DiscoveryRuntimeSettingsPanel>(composed byviews/discovery/). - Own the small inline UI affordances unique to discovery actions (delete-confirmation copy, restore toast).
What it does NOT own:
- Reading the jobs list (that is
operations/). - Stage state transitions beyond delete/restore (that is
pipeline/). - Score, materials, or apply concerns.
Folder rationale. The folder exists because the backend bounded context exists and DeleteJob / RestoreJob / ImportJob and the source-registry use cases are Discovery-context use cases per backend §5.1 — not Operations and not Pipeline. What began as a thin delete/restore surface has since grown into the full discovery-administration surface described above; the folder absorbed that growth without restructure.
3.3 Job Enrichment (Frontend)
Backend mirror: Job Enrichment (backend domain model §3.2, §4.2, §5.2).
Purpose: Consume enrichment events into the invalidation router; surface compensation evidence; and expose manual re-enrichment and compensation-refresh actions.
Ubiquitous language (matches backend):
- JobEnrichment — the aggregate (one per Job).
- EnrichmentAttempt — child entity, one per try.
- ExtractionTier —
json_ld | css_selectors | llm_assisted. - FullDescription, ApplicationUrl — value objects.
Responsibilities:
- Register invalidation handlers (in
contexts/enrichment/handlers.ts, wired throughoperations/invalidation-router.ts) forJobEnriched,EnrichmentFailed,PostingContentSnapshotCaptured/PostingContentSnapshotFailed,JobActiveStateChanged,ContentDuplicateCandidateDetected, andCompensationFactsUpdated. - Wrap the compensation-refresh actions in
useRefreshCompensationMutation/useRefreshAllCompensationMutation. The manualEnrichJobUseCaseretry hookuseEnrichmentRetryMutationexists as a stub (throwsNotImplementedError) pending its backend endpoint. - Own the compensation-evidence UI:
<CompensationSummaryCell>,<CompensationSummaryStrip>,<CompensationAuditSection>, and<RefreshAllCompensationButton>(composed by the Jobs drawer and the Apply Review view).
What it does NOT own:
- Reading the jobs list / detail (that is
operations/). - Scoring, materials, or apply concerns.
Folder rationale. The backend context exists and EnrichJobUseCase is a §5.2 use case; the enrichment folder now carries real compensation UI and refresh mutations (with the manual re-enrichment retry hook still a stub pending its backend endpoint), and remains the unambiguous home for any further enrichment-triggered affordance.
3.4 Candidate Profile
Purpose: Read, edit, and import the candidate profile; manage application preferences, settings, credentials, and resume template defaults; preview the rendered resume PDF.
Ubiquitous language (matches Candidate Profile):
- Profile — the candidate document.
- Preferences — application defaults, target-role strategy, tailoring controls, and resume-rendering preferences that affect how JobCtrl acts for the candidate.
- ExperienceEntry, EducationEntry, SkillCategory — child entities.
- TailoringPolicy, WritingStyle, ResumeConstraints — value objects.
- ResumeTemplate — versioned style/layout configuration for resume PDF rendering. Template preview may use profile data for display, but saved template payloads contain style/layout only.
- ResumeImportWizard — the multi-step flow for importing a profile from a resume PDF (frontend-only presentation term; the underlying backend use case is
ImportProfileUseCase).
Backend mirror: Candidate Profile (backend domain model §3.3, §4.3, §5.3).
Responsibilities:
- Render the profile editor with TanStack Form.
- Drive the resume-import wizard as a nested route (
/profile/import/upload,/profile/import/preview,/profile/import/confirm) so each step is bookmarkable and refresh-safe. (Resolves §6 question 8.) - Render the top-level Preferences route (
/preferences) for application defaults, target preferences, AI tailoring controls, and resume style. The route edits the same profile/style payloads but keeps those controls out of the Profile view, whose UI is reserved for who the candidate is. - Render the resume-template editor in Preferences, backed by the same Plate HTML/CSS profile preview surface and Profile-owned query/mutation hooks for template list, version save, and default selection.
- Show the live baseline resume Plate editor by reading generated HTML from a
cacheKey-versioned Profile preview URL derived from the profile mutation count (see §4.4.4 for the precise binding pattern). - Host settings and credentials hooks. Settings and credentials are surfaced via peer routes (
/settings,/settings/credentials); their hooks and forms live insidecontexts/profile/becauseapiClient.settings*andapiClient.credentials*are part of the candidate-profile API surface area inapps/api/.
What it does NOT own:
- Per-job tailored content (that is
materials/). - Score breakdown or correction (that is
scoring/).
3.5 Scoring
Purpose: Render fit scores, the score breakdown and reasoning, score staleness, score correction, and rescore actions.
Ubiquitous language (matches Scoring):
- FitScore — 1–10 integer.
- ScoreBreakdown — structured
technicalFit,experienceFit,reasoning. - MatchedKeywords — ATS keywords matched.
- ScoreCorrection — user-provided override (
CorrectScoreUseCaseper backend §5.4; shipped viauseCorrectScoreMutation+<ScoreCorrectionControl>).
Backend mirror: Scoring (backend domain model §3.4, §4.4, §5.4).
Responsibilities:
- Provide the scoring render components —
<ScoreBadge>(jobs table cell- drawer),
<ScoreReasoning>, and<ScoreStalenessBadge>.<ScoreBreakdown>is built, tested, and storied but is not yet composed by any view (the Jobs drawer surfaces scoring through<JobAuditTriage>→<ScoreCorrectionControl>); it stays available for wiring.
- drawer),
- Own score-correction and rescore mutations:
useCorrectScoreMutation(shipped) plususeRescoreJobMutation,useRescoreCurrentPolicyMutation, anduseResetStaleScoresForRescoreMutation, surfaced through<ScoreCorrectionControl>,<RescoreJobButton>,<RescoreCurrentPolicyButton>, and<ResetStaleScoresButton>. - Own
<CompensationSourcePolicyPanel>(readsuseCompensationSourcePolicyQueryfrom Operations).
What it does NOT own:
- The generic per-stage
scoreretry (that is apipeline/retry-stage action); scoring owns the domain-level rescore/correction actions above. - The fit-score column header in the jobs table (that is
views/jobs/; scoring contributes the cell renderer only).
3.6 Materials Generation
Purpose: Trigger generation / re-tailoring of resume + cover letter + PDFs for a given job; observe progress; open generated artifacts in the OS default app.
Ubiquitous language (matches Materials Generation context in backend domain model §4.5):
- MaterialsSet — the generation set for one job, identified by
(TenantId, JobId, generation). - Generation — the version counter on
MaterialsSet. - TailoredResume, CoverLetter — value objects.
- Artifact — child entity within
MaterialsSet; identified byartifactId. - ArtifactStatus —
candidate | approved | rejected | superseded. - ArtifactType —
tailored_resume | cover_letter | resume_pdf | cover_letter_pdf. - ValidationResult — banned-words / fabrication / structural check.
- JudgeVerdict — LLM-as-judge evaluation of the tailored resume.
Backend mirror: Materials Generation (backend domain model §3.5, §4.5, §5.5).
Responsibilities:
- Wrap
apiClient.generateMaterials(jobId, ...)in a mutation hook. - Wrap
apiClient.openArtifact(artifactId)in a mutation hook (artifacts are owned byMaterialsSet; the OS-open action is materials-context surface even though it surfaces in the Artifacts view). - On
generatesuccess, do not refetch eagerly — let the SSE stream (ResumeApproved,CoverLetterGenerated,PdfRendered,MaterialsExhausted) drive query-cache invalidation. The mutation resolves withrunId; the UI shows "queued" until the corresponding events arrive (§7).
What it does NOT own:
- Reading the artifacts list (that is
operations/—useArtifactsListQuery,useArtifactDetailQuery). - Apply submission (that is
apply/; apply consumes the latestMaterialsSetartifacts).
3.7 Apply Automation
Purpose: Trigger apply / dry-run for a job; cancel a running apply; observe an apply run's live event timeline.
Ubiquitous language (matches Apply Automation / ApplyRunProjection):
- ApplyRun — one apply attempt, identified by
(TenantId, RunId). - DryRun — apply attempt that does not submit.
- SubmissionResult —
applied | failed | captcha | login_issue | expired | manual | dry_run. - ApplyRunEvent — telemetry event within an apply run.
Backend mirror: Apply Automation (backend domain model §3.6, §4.6, §5.6).
Responsibilities:
- Mutation hooks:
useApplyJobMutation,useDryRunApplyMutation,useCancelApplyMutation. - Render the apply timeline (events, tokens, cost) for a selected apply run via
<ApplyRunTimeline>. Live-update the timeline by subscribing toApplyRunEventRecordedevents scoped torunId.
What it does NOT own:
- Reading the apply runs list (that is
operations/—useApplyRunsListQuery, derived from the dashboard summary; there is nouseApplyRunQuery). The high-frequencysetQueryDatapatching forApplyRunEventRecordedmutatesoperations/-owned cache, but the routing rule lives in the invalidation router. - "Mark applied" / "mark skipped" — those are pipeline-state transitions per backend §5.7 and live in
pipeline/.
3.8 Pipeline Orchestration
Purpose: Render stage state badges; expose stage-management mutations (retry, cancel, mark-applied, mark-skipped); render the dashboard funnel detail.
Ubiquitous language (matches Pipeline Orchestration):
- Stage —
discover | enrich | score | tailor | cover | pdf | apply. - StageState — discriminated union (Pending, Queued, Running, Succeeded, Failed, Blocked, Skipped, Exhausted, Stale, Canceled).
- NextAction — recommended action when blocked/failed.
- AttemptCount — current attempt index.
Backend mirror: Pipeline Orchestration (backend domain model §3.7, §4.7, §5.7).
Responsibilities:
- Provide
<StageBadge state={...} />(exhaustiveswitchonstate.kind) and<StageTimeline stages={...} />. - Provide
<JobActions jobId={...} />toolbar composer assembling per-stage / per-action buttons across pipeline + materials + apply contexts. - Mutations:
useRetryStageMutation,useCancelStageMutation,useMarkAppliedMutation(MarkAppliedUseCaseper backend §5.7),useMarkSkippedMutation(SkipJobUseCaseper backend §5.7). - Render the funnel widget on the dashboard (composed from
DashboardProjection.funnel). - Render the copyable CLI command for any stage's
nextActionvia<CopyableCommand command={stage.nextAction} />fromshared/ui/. This preserves the affordance recorded indocs/decisions.md(2026-05-03): "copyable commands stay; buttons use structured actions." Buttons call mutation hooks (structured); the copyable strip remains for transparency and manual debugging.
What it does NOT own:
- Apply submission (that is
apply/; apply is a stage but its UI affordance is a domain action, not a stage transition button). - Score correction (that is
scoring/).
3.9 Operations / Read-Side
Purpose: Provide the read-side kernel to all other contexts and views: the query client, query-key registry, projection-typed hooks, and the SSE subscription that fans events out to invalidate query keys.
Ubiquitous language (matches Operations / Read-Side):
- Projection — denormalized read shape (
JobListProjection,JobDetailProjection,DashboardProjection,ArtifactListProjection,ApplyRunProjection). - EventStream — the SSE channel publishing
DomainEvents. - InvalidationRouter — pure function that maps an incoming event to the set of query keys to invalidate.
- QueryKeyRegistry — the aggregation of every query-key factory, exported from
contexts/operations/queryKeys.ts. Today it re-exports 17 factories: the read-side ones owned locally by Operations (jobsKeys,artifactsKeys,dashboardKeys,applyRunsKeys,applyReviewKeys,activityKeys,outcomesKeys,workflowRunsKeys,healthKeys,compensationKeys) plus each aggregate context's own factory (profileKeys,discoveryKeys,enrichmentKeys,scoringKeys,materialsKeys,applyKeys,pipelineKeys).
Backend mirror: Operations / Read-Side (apps/api/src/projections.ts, workers/automation/.../infrastructure/projections/).
Responsibilities:
- Configure the
QueryClient(defaults:staleTime,gcTime, retry policy, error handler). - Mount the
<EventStreamProvider />that opens the SSE connection on application start, scoped byTenantId. - Implement the invalidation router that consumes events and calls
queryClient.invalidateQueries({ queryKey })(orsetQueryData(...)for high-frequency events). - Own all projection-typed read hooks. The core set:
useDashboardSummaryQuery,useJobsListQuery,useJobDetailQuery,useArtifactsListQuery,useArtifactDetailQuery,useApplyRunsListQuery,useActivityListQuery/useActivityEventQuery,useWorkflowRunsListQuery/useWorkflowRunDetailQuery,useApplyReviewQueueQuery,useResumeReviewDraftQuery,useApplicationOutcomesQuery/useJobApplicationOutcomesQuery,useCompensationSourcePolicyQuery,useHealthQuery, and the discovery-product-control read hooks inuseDiscoveryProductControlsQuery(source registry, source-locator candidates, source preview, quarantine, manual-capture queue, role-match feedback). Note there is no standaloneuseApplyRunQuery: apply-run detail is not yet a dedicated endpoint, souseApplyRunsListQueryderives runs from the dashboard summary viaselect, and per-run live events are patched ontoapplyRunsKeys.detail(...)by the invalidation router (§7.5). - Re-export projection and API response types (sourced from
@jobctrl/domain-typesvia@jobctrl/contracts) throughcontexts/operations/types.tsso feature contexts and views can import them from one place. (This is the frontend Anti-Corruption Layer — §6.5.)
What it does NOT own:
- Any feature UI. Operations is pure infrastructure code: hooks, types, router.
- Any mutation. Mutations are owned by the aggregate context they correspond to (Discovery, Materials, Apply, Pipeline, Profile).
3.10 Views (Composition Layer — NOT Bounded Contexts)
The views/ folder holds the sibling composers of contexts/, including dashboard/, jobs/, artifacts/, apply-review/, runs/, pipelines/, discovery/, outreach/, and debug/. They are not bounded contexts; they are presentation composers. The dichotomy is intentional and binding:
- A context owns a slice of the backend's domain language and the hooks/components that surface it. It maps 1:1 to a backend bounded context.
- A view owns layout, composition, and view-local ephemeral UI state (e.g., bulk-selection sets that intentionally do not survive navigation). It imports hooks from
contexts/operations/and components/mutations from aggregate contexts.
View → context dependency direction is one-way. A view depends on contexts; a context never depends on a view. A view never depends on another view (cross-view navigation goes through the URL).
The views:
| View | Composition |
|---|---|
views/dashboard/ | <KpiGrid>, <ConversionPanel>, <Funnel>, <SourceHealthCard>, <ApplyRunsCard> (operations: useDashboardSummaryQuery), plus an outcome-suggestions section (operations: useApplicationOutcomesQuery; apply: <OutcomeSuggestionsPanel>). Funnel/ApplyRunsCard compose pipeline <StageBadge> and apply <ApplyRunBadge>. |
views/jobs/ | <JobsTable> (operations: useJobsListQuery; column cells use <ScoreBadge>/<StageBadge>; product filters bind to URL state), <JobBulkActions> (discovery: delete / hide / restore / permanent-delete bulk mutations), <JobDetailDrawer> (composes <JobOverview> + <JobActions> + <StageTimeline> + artifact badges + <EmployerAnalysisPanel> + <ApplyHistory> + <JobOutcomePanel> + <JobAuditHistory>). |
views/artifacts/ | <ArtifactsTable> (operations: useArtifactsListQuery), <ArtifactFilterBar> (URL-bound), <ArtifactDetailPanel> (operations: useArtifactDetailQuery; materials: useOpenArtifactMutation, <TailoringExplanationSection>). |
views/apply-review/ | <ApplyReviewView> — the human apply-approval workstation. Left pane is the review queue (operations: useApplyReviewQueueQuery); right pane composes the live Plate resume editor (<ResumePlateEditor> from materials, wired to the apply-context draft/comment/reply/render mutations via useResumeReviewDraftQuery), grounding-risk + requirement audit panels, <ApplyReviewDecisionControls>, and <CancelApplyButton>. |
views/runs/ | <RunsView> — unified workflow-runs browser. <RunsTable> (operations: useWorkflowRunsListQuery), <RunsFilterBar> (URL-bound), per-row <CancelWorkflowRunButton> ("Stop") and a Temporal Web-UI deep link; <WorkflowRunDrawer> (operations: useWorkflowRunDetailQuery) at /runs/$runId. |
views/pipelines/ | <PipelinesView> — renders the pipeline context's <StageTriggerPanel> (global/batch stage triggers + <CancelWorkflowRunButton>). |
views/discovery/ | <DiscoveryView> — stacks <TargetSearchSettingsPanel> + <DiscoveryAutomationSettingsPanel> (profile), <DiscoveryRuntimeSettingsPanel> + <DiscoveryProductControls> (discovery). |
views/debug/ | <DebugActivityTable> (operations: useActivityListQuery), <DebugFilterBar> (URL-bound), <ActivityDetailDrawer> (operations: useActivityEventQuery) at /activity/$eventId. |
views/outreach/ | <OutreachView> — the Contacts page. <OutreachTable> (outreach reads; provenance-summary + role-badge cells), <OutreachDetailDrawer> (outreach: contact detail; renders provenance for every fact and composes the outreach context's <OutreachThreadPanel> for draft review) at /outreach/$contactId. The outreach context's <JobContactsPanel> also composes into the Jobs drawer. |
The view's only owned components are layout and view-local affordances (filter bar, bulk-action toolbar). All cell renderers, badges, drawers, forms, and timelines are imported from the contexts that own them.
3.11 Contact & Outreach (Frontend)
Backend mirror: Contact & Outreach (backend domain model §3.11, §4.9, §5.9).
Purpose: Surface the Contact aggregate — keep recruiter / hiring-manager / referrer contact records per company or application, render every fact with its provenance, and import contacts from a CSV file — plus supervised research (Phase 2) and the truthful outreach draft review surface (Phase 3). There is no sending in the UI: drafts terminate at an approved, copyable message, and the product never sends.
Ubiquitous language (matches backend):
- Contact — the
Contactaggregate identified by(TenantId, ContactId). - ContactAttribute — one fact (name, title, email, phone, profile URL, note) with mandatory provenance.
- ContactFactProvenance —
sourceKind/sourceRef/captureMethod/capturedAt/confidence/userConfirmed. - ContactRole —
recruiter | hiring_manager | referrer | warm_intro | other.
Responsibilities:
- Contact read hooks, context-owned (mirroring how Profile owns
useProfileQuery):useContactsListQuery,useContactDetailQuery. - Contact mutation hooks via
createOptimisticMutationwith real patchers + settle sets:useCreateContactMutation,useUpdateContactMutation,useDeleteContactMutation,useImportContactsMutation. - Query keys:
outreachKeys(hierarchical, tenant-first), re-exported throughcontexts/operations/queryKeys.ts. - Handlers:
contexts/outreach/handlers.ts, registered in the invalidation router. - Forms & store: TanStack Form + Zod
safeParse(contact-form,contact-import-wizard); a Zustandpersiststore (outreach-import-store, keyjh:outreach-import) for the multi-step CSV-import wizard. - Components:
<ContactRoleBadge>,<ContactProvenanceList>/<ContactProvenanceSummary>(render provenance for every fact — INV-2), the create / edit / delete / import buttons, and<JobContactsPanel>(composed into the Jobs drawer). - Outreach draft read + mutation hooks (Phase 3):
useOutreachThreadQuery;useGenerateDraftMutation,useReviseDraftMutation,useApproveDraftMutation,useRejectDraftMutation(viacreateOptimisticMutationwith real patchers + settle sets). - Draft review components (Phase 3):
<OutreachThreadPanel>(the review surface),<DraftGateResultsPanel>,<DraftClaimProvenanceList>,<DraftStatusBadge>, the<GenerateDraftButton>/<ApproveDraftButton>/<RejectDraftButton>/<CopyDraftButton>actions, and therevise-draft-form. - Send-log + follow-up hooks (Phase 4):
useDueFollowUpsQuery;useLogSendMutation,useScheduleFollowUpMutation,useCompleteFollowUpMutation,useDismissFollowUpMutation(viacreateOptimisticMutationwith real patchers —markThreadSentInThread/setThreadFollowUpInThread— + settle sets that includedueFollowUps). - Send-log + follow-up components (Phase 4): the
send-log-form(records a user-attested send — copy makes clear the user sends externally and this only records the fact),<OutreachSendLogList>(send history),<FollowUpPanel>(schedule / complete / dismiss), and<DueFollowUpsPanel>/<DueFollowUpsBadge>(the surfaced due-follow-ups list + count, composed into<OutreachView>). There is still no send affordance — logging records a fact (INV-1).
What it does NOT own:
- Job / dashboard / artifact / apply-run projections (those stay in
operations/). - Any send/transport affordance — none exists; a thread becomes "sent" only through a user-attested send log the user records (INV-1).
Audit rendering: the contact list and detail render provenance for every stored fact (INV-2); attribute values reach the UI only through the read DTOs and never leave the local machine.
Outreach draft review (Phase 3): <OutreachThreadPanel> (composed into the contact detail drawer, views/outreach/OutreachDetailDrawer.tsx) is the draft review surface. It shows the current approved draft prominently, the latest candidate under review with its <DraftGateResultsPanel> (the persisted truthfulness-gate outcome) and <DraftClaimProvenanceList> (claim → fact bindings), and the full generation history so a re-draft never hides a prior generation (INV-5). Approve stays disabled until the gates pass; editing opens the revise-draft-form, which submits a new generation that re-runs the gates. Copy-to-send goes through the ClipboardPort (usePorts().clipboard, via <CopyDraftButton>, enabled only for an approved draft) — a user-initiated clipboard write, never navigator.clipboard directly and never a network send. There is no send action anywhere (INV-1).