Skip to content

Data Model

Entity Relationship

erDiagram
    User ||--o{ Job : owns
    User ||--o{ Resume : owns
    User ||--o{ Application : owns
    Job ||--o| JdAnalysis : analyzed
    Job ||--o{ Application : applied
    Resume ||--o{ TailoredResume : tailored
    Resume ||--|{ ResumeSection : contains
    Resume ||--o{ SkillEntry : "generates profile"
    TailoredResume ||--|{ TailoredSection : contains
    TailoredResume }o--|| Job : for
    Application }o--o| TailoredResume : uses

    User {
        uuid id PK
        string email
    }

    Job {
        uuid id PK
        uuid owner_id FK
        string platform
        string platform_job_id
        string title
        string company
        string location
        int salary_min
        int salary_max
        string url
        text description_raw
        bool pr_required "nullable"
        bool citizenship_required "nullable"
        bool visa_sponsorship "nullable"
        string status
        datetime scraped_at
    }

    JdAnalysis {
        uuid job_id FK
        jsonb required_skills
        jsonb preferred_skills
        int years_experience
        jsonb keywords
        float match_score
        text match_reasoning
    }

    Resume {
        uuid id PK
        bool is_base
        string format
    }

    ResumeSection {
        uuid id PK
        uuid resume_id FK
        string section_type
        text content
        bool tailorable
    }

    TailoredResume {
        uuid id PK
        uuid base_resume_id FK
        uuid job_id FK
        text diff_summary
        bool approved
    }

    Application {
        uuid id PK
        uuid job_id FK
        uuid tailored_resume_id FK
        string status
        datetime applied_at
    }

    SkillEntry {
        uuid id PK
        string name
        string category
        int proficiency "1-10"
        float years
        jsonb evidence
        string source
    }

Key Entities

User

The owner of every Job, Resume, and Application. Single-user mode today (one fixed User with id 00000000-0000-0000-0000-000000000001, see kairos.api.auth.SINGLE_USER_ID); the schema is multi-tenant-ready so the SaaS migration is a no-op at this layer.

class User(BaseModel):
    id: UUID
    email: str

Job

The central entity. Tracks a job posting from discovery through application. Every query is scoped by owner_id; the unique constraint is (owner_id, platform, platform_job_id) so two users can independently track the same listing without collision.

class Platform(StrEnum):
    SEEK = "seek"
    LINKEDIN = "linkedin"
    INDEED = "indeed"
    ADZUNA = "adzuna"

class JobStatus(str, Enum):
    NEW = "new"              # Just scraped
    ANALYZED = "analyzed"    # JD analyzed by LLM
    MATCHED = "matched"      # User marked as interesting
    SKIPPED = "skipped"      # User passed
    APPLIED = "applied"      # Submitted
    REJECTED = "rejected"    # Got rejection
    INTERVIEW = "interview"  # Interview scheduled
    OFFER = "offer"          # Received offer

JdAnalysis

LLM-generated analysis attached 1:1 to a Job. Key fields:

  • required_skills / preferred_skills — extracted from JD (JSONB)
  • keywords — ATS keywords to include in resume (JSONB)
  • match_score — 0.0 to 1.0, computed against UserProfile
  • match_reasoning — human-readable explanation

Resume & TailoredResume

Base resume imported once and split into sections. Each section marked tailorable: True/False.

Tailored resumes only store modified sections — unchanged sections inherit from base at render time.

class SectionType(str, Enum):
    SUMMARY = "summary"                    # tailorable
    EDUCATION = "education"                # protected
    WORK_EXPERIENCE = "work_experience"    # tailorable (bullets only)
    INTERNSHIP = "internship"              # tailorable (bullets only)
    PROJECTS = "projects"                  # tailorable
    TECHNOLOGIES = "technologies"          # tailorable
    EXTRACURRICULAR = "extracurricular"    # protected
    REFERENCES = "references"              # protected

Application

Tracks the lifecycle from preparation to outcome.

class ApplicationStatus(str, Enum):
    PREPARED = "prepared"          # Resume tailored, ready
    SUBMITTED = "submitted"        # User confirmed
    ACKNOWLEDGED = "acknowledged"  # Company acknowledged
    INTERVIEW = "interview"        # Scheduled
    REJECTED = "rejected"
    OFFER = "offer"

Skill Profile

A structured representation of the user's technical abilities, auto-generated from resume analysis and adjustable manually. Used by JD Analysis to compute precise match scores.

class SkillCategory(str, Enum):
    LANGUAGE = "language"        # Rust, Python, TypeScript
    FRAMEWORK = "framework"      # Django, React, FastAPI
    DATABASE = "database"        # PostgreSQL, SQL Server
    TOOL = "tool"                # Docker, Git, CI/CD
    CLOUD = "cloud"              # AWS, GCP, Azure
    CONCEPT = "concept"          # System Design, Async, REST API

class SkillSource(str, Enum):
    RESUME_ANALYSIS = "resume_analysis"  # Auto-extracted from resume by LLM
    MANUAL = "manual"                    # User-provided
    BOTH = "both"                        # Auto-extracted then user-adjusted

Each SkillEntry includes: - proficiency — 1-10 scale (1 = awareness, 5 = working proficiency, 8 = expert, 10 = authority) - years — years of experience with this skill - evidence — specific projects/roles that demonstrate this skill (e.g. "Efision ERP — async/tokio/axum") - source — whether it was auto-extracted, manually added, or both

PostgreSQL Schema

JSONB columns for structured data (skills, keywords, evidence). UUIDs as native uuid type. Timestamps as timestamptz.

Migrations live at alembic/versions/:

  • 001_initial_jobs.py — initial Job table
  • 002_add_cursor_pagination_index.py(scraped_at, id) index for keyset pagination
  • 003_add_owner_id_to_jobs.py — adds owner_id, backfills the single-user UUID, swaps the unique constraint to (owner_id, platform, platform_job_id)