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.
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 UserProfilematch_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 table002_add_cursor_pagination_index.py—(scraped_at, id)index for keyset pagination003_add_owner_id_to_jobs.py— addsowner_id, backfills the single-user UUID, swaps the unique constraint to(owner_id, platform, platform_job_id)