Skip to content
Xingchi Guo
All projects

Career Search & Interview Workflow System

CareerNeed

A full-stack product that models a job search as one connected workflow — sources, jobs, career directions, resumes, applications, and interviews — with AI analysis that has to cite its evidence before it is shown.

Status
In development
Stack
  • Next.js
  • React
  • TypeScript
  • Python
  • FastAPI
  • SQLAlchemy
  • PostgreSQL
  • Alembic
  • Docker
  • GitHub Actions
  • LLM integration
  • Structured outputs

The problem

A job search produces a surprising amount of state. Roles are found on different job boards. Each one becomes a saved link, then maybe a row in a spreadsheet. Resumes multiply into versions. Applications live in other companies' portals. Interview notes go in a document, follow-ups in a calendar, and analysis in an AI chat that knows nothing about any of the rest.

None of those artifacts is hard to manage alone. The cost is in the gaps between them: which resume went to which role, what was promised in the last conversation, what is due this week.

CareerNeed treats the search as a single structured workflow, with real records and relationships underneath, instead of a set of bookmarks with a chat box attached.

Product model

The domain separates what is shared from what belongs to one person, and what is a fact from what is derived.

  1. Sources and catalog

    • Company source

      A tracked company and its job-board configuration, owned by one account.

    • Job

      A normalized posting. Shared and read-only to normal clients.

    • Job classification

      Derived role metadata with its own run history. Can abstain.

    a person acts on a job through

  2. Owned by one account

    • Career direction

      A search track: role, seniority, optional resume, lifecycle.

    • Resume

      Uploaded PDF with extracted text, label, default, archive.

    • Application

      The person's own pipeline record for one job.

    an application accumulates

  3. Around an application

    • Contacts

      People connected to the opportunity.

    • Follow-ups

      Typed next actions with dates.

    • Interviews

      Rounds with participants and questions.

    analysis is recorded as

  4. AI runs

    • Match brief run

      An immutable snapshot of one resume and one job, plus the validated result.

    • Interview preparation runIn progress

      Snapshot foundation for preparation briefs.

Jobs are a shared catalog. Everything a person does with a job is a separate, owner-scoped record.

Two choices in this model do most of the work. A Job is a shared reference that normal clients cannot edit or delete, while an Application is private to its owner. And anything inferred — a role classification, an AI analysis — is stored as derived data with its own history, next to the source records rather than on top of them.

Architecture

  1. Client

    • Next.js web app

      React and TypeScript. Server-rendered pages and client views.

    same-origin /api/*

  2. API

    • FastAPI routers

      Authenticated, owner-scoped endpoints.

    • Domain services

      Source sync, classification, match briefs, interview preparation.

    SQLAlchemy

  3. Data

    • PostgreSQL

      Schema managed by Alembic migrations, with database-level invariants.

    outbound only, from the API

  4. External services

    • Job boards

      Ashby, Greenhouse, Lever.

    • LLM providers

      OpenAI and Google Gemini, using the user's own key.

    • Identity provider

      Verifies sign-in; never owns business data.

One public origin. The browser never talks to a provider directly.

The frontend is the public origin and proxies /api/* to the API. That keeps authentication first-party and lets server-rendered pages make authenticated requests without cross-site cookie behavior.

Job ingestion

The interesting part of ingestion is not fetching postings. It is turning three different providers into one model, behind one safe entry point.

  • Three importers, one job model. Ashby, Greenhouse, and Lever each have their own importer. All of them write the same normalized Job fields — company, title, location, workplace type, description, application link, and first-seen and last-seen timestamps — and deduplicate on the provider's external id.
  • Manual jobs are first-class. Adding a job by hand creates a shared catalog entry together with the creator's private application, in one step.
  • One write path. Synchronization happens through a single authenticated, owner-scoped endpoint for a single stored source. The browser sends only the source id; provider type and configuration come from the server's stored record.
  • A retired shortcut. An earlier set of provider-wide sync routes accepted caller-supplied configuration without authentication. They were removed, and regression tests assert they now return 404.
  • Classification runs after ingestion, in isolation. A deterministic, title-only classifier runs in its own transaction after an import commits, so a classification failure cannot fail the import.

Synchronization is an explicit, per-source action. Nothing scans or applies automatically.

Career directions

People rarely run one job search. A frontend-leaning search and a platform-leaning search want different titles, a different seniority, and often a different resume.

A career direction makes that a record instead of a habit. It carries a role chosen from a versioned reference taxonomy (or a custom one), a seniority, an optional resume, and a lifecycle of active, paused, or archived. At most one active direction is the default, and that rule is enforced by the database rather than by the interface.

Its integration with job search is deliberately narrow: a direction can seed the editable search filters once, as a snapshot. It is not a live sync, and it is kept independent of job classification.

From job to application

Workflow

  1. Source

    Tracked company or manual entry.

    then
  2. Job

    Searched, filtered, saved filters.

    then
  3. Match brief

    Optional, against a chosen resume.

    then
  4. Application

    Private pipeline record.

An application is a workflow record, not an "applied" flag on a job. It has its own status, notes, an associated resume, contacts, and typed follow-ups — a thank-you, a status check, a recruiter reply, preparation, or a custom action. The same applications can be viewed as a list, a pipeline board, or a detail page, and a separate view gathers what is due today.

Because the job is shared and the application is private, two people can track the same posting without seeing anything of each other's process.

Interviews

Interview support is the area where the line between built and in progress matters most.

Implemented. Interviews are records attached to an application, with a type, a status, participants, and questions. They feed the "today" and follow-up views, and the system can produce preparation and outcome suggestions.

In progress. The interview preparation brief has a structured backend: bounded context, a typed output schema, validation that references point to allowed sources, safe error mapping, and audit records. It is covered by mock-based tests, and its quality against live providers has not yet been validated.

Foundation only. Preparation runs now store immutable snapshots of their inputs, and a resolver maps an interview to a reference interview type. Nothing generates from those snapshots yet.

AI as a workflow layer

AI in CareerNeed is not a chat surface. It is a set of runs attached to specific records, and the central feature is the match brief: an analysis of one resume against one job.

Match brief

  1. Run snapshot

    Frozen copy of the resume and job, fingerprinted.

    then
  2. Evidence segments

    Sources split into referenceable pieces.

    then
  3. Provider adapter

    Structured generation with the user's key.

    then
  4. Validated result

    Schema and citations checked, or the run fails.

  • Runs are immutable. Each run snapshots its inputs, so a later edit to a resume cannot change what an old analysis was based on. The interface shows when a source has since gone stale.
  • Claims must cite. Sources are divided into a versioned registry of segments. The model's output has to reference those segments, and every reference is validated against the exact run it belongs to. The result page resolves each citation back to the excerpt.
  • Invalid output is a failed run. Output that breaks the schema or cites something that does not exist is rejected. There is no partial result and no silent downgrade.
  • Role-aware analysis is opt-in by eligibility. A second version adds competency alignment, but only when the job has a current, non-stale role classification. Otherwise the run explicitly uses the first version.
  • Bring your own key. Users supply their own provider credentials, which are encrypted at rest. A model router sits in front of provider adapters — OpenAI and Google Gemini today — and maps provider failures to a fixed set of safe error codes. Usage is logged per run.
  • Development has a fake provider. A deterministic, credential-free adapter exercises the full production path — prompt, schema, validation, persistence — and is unavailable outside development and test.

Evaluation is treated the same way as the feature. There is a frozen synthetic benchmark with a recorded digest and tooling to run and review it, but the real-provider baseline and human review have not been done. So this page makes no claim about how accurate the analysis is.

Data isolation and auth

Every private record — resumes, applications, directions, interviews, contacts, match briefs, saved filters, AI credentials — carries its owner, and every route filters by the authenticated account. Cross-user tests cover the major private resources.

Authentication is kept separate from product-owned account data. Identity is verified at the application boundary, and what comes out of that check is mapped to a CareerNeed account through a dedicated table. Business records only ever reference the account, never an external identity, and an existing account is never linked to a new sign-in on the strength of a matching email alone.

Sign-in through external identity providers is being integrated on top of this boundary. Because ownership is expressed in terms of the account, adding a provider does not change how any record is owned or filtered.

Engineering decisions

A shared job, a private application

Why
A posting and one person's process around it are different things with different owners and lifecycles.
Tradeoff
Two records to keep coherent, and no ordinary endpoint for editing a shared job.

Career direction as a first-class record

Why
Different searches need their own role, seniority, and resume. The single-default rule is enforced in the database.
Tradeoff
More relationships and interface state than one global profile, and its coupling to job search is intentionally limited.

One owner-scoped sync path behind normalized importers

Why
Providers differ, and letting a caller choose provider configuration was an unsafe parallel path. It was removed.
Tradeoff
Each provider needs its own importer to maintain, and there is no bulk or automatic synchronization.

AI output must validate and cite, or the run fails

Why
An analysis is only useful in a workflow if each statement can be traced to the resume or the job it came from.
Tradeoff
Versioned schemas, a segment registry, and more failed runs than a free-text answer would produce.

Bring-your-own-key behind a provider router

Why
Product logic should not depend on one model vendor, and users keep control of their own usage.
Tradeoff
Encrypted credential storage, provider-specific adapters, and error handling for each provider's quirks.

Delivery and reliability

  • Continuous integration runs against a real database. The backend job starts PostgreSQL, applies every migration, runs the test suite, lints and format-checks changed Python files, and then checks for drift between the models and the migrations.
  • The frontend job is a full gate. Tests, formatting, lint, generated route types, a TypeScript check, and a production build.
  • Both services are containerized. The API and the web app each have a provider-neutral Dockerfile with no secrets baked in.
  • Staging is specified as a contract. The required environment, the same-origin /api routing, secure-cookie settings, and the migration step are documented, with secrets supplied by the host.
  • Startup fails closed. The reference taxonomy is verified by digest when the API starts; a mismatch stops the service instead of serving inconsistent data.

CareerNeed is an evolving application. Its tests and CI establish that the implementation behaves as designed. They are not a claim of production readiness, and the project's own status document says so.