Skip to content
Architecture

Data model

The tables behind every page and the fields that matter.

Everything lives in one SQLite file, jobhunter.db, in WAL mode with a 30-second busy timeout. Rows are never deleted — they are hidden, merged or quarantined, so history is never lost.

How the tables relate#

company ─┬─< job roles it posts
├─< contact people who work there
│ └─< email asks written to them
│ ├─< reply what came back
│ └─< event a “yes” with a time
└── field_meta evidence for every field

company#

The centre of everything. Its columns group into a few concerns:

GroupKey fields
Identityname (unique) · website · domain · github_org · ats · ats_slug · linkedin_url · city
Targetingtier · tier_reason · overrated · priority_score · pay_power · pay_power_band · money_per_head_usd_k
Fundingfunding_stage · funding_amount_usd_m · funding_announced · funding_investors · valuation_usd_m · team_size
Hiringcareers_url · hiring_status · hiring_claim(_by/_source/_at) · hiring_roles · hiring_checked_at
Addressesemail_pattern (only when confirmed) · email_pattern_evidence
Lifecyclestatus (null = active | quarantined | merged | below_pay_bar) · merged_into · retry_state · do_not_contact
Completenesscomplete · complete_missing · completeness · fill_state (why parts are missing) · filled_at
Listslist_key · list_rank · list_meta (the ★ India hand-picked list)
Evidencefield_meta — JSON per field: value, evidence, source, channel, time, confidence, agreeing sources

job#

GroupKey fields
Postingcompany_id · title · location · remote · url (unique) · fingerprint · source · description · posted_at
Paysalary_min_lpa · salary_max_lpa · is_internship · stipend_inr_month · ppo_lpa
Flagsis_senior · remote_anywhere
Matchingembed_sim (the lexical gate score — the name is historical) · match_score · match_reasons · skill_gaps · status
Selectionselection_score · selection_band · selection_market · selection_why · needs_brief

contact#

GroupKey fields
Identitycompany_id · name · role · email · github · linkedin · x_handle · handle
Addressconfidence (verified | pattern-guessed | scraped) · email_kind · source
Rolerole_class · seniority · referral_rank (1 = ask first) · role_evidence
Employmentaffiliation (verified-current | likely-current | unconfirmed | generic) · affiliation_evidence · last_commit_at
Suppressionemail_status (bounced | opted_out) · dead_email · do_not_contact

email, reply, event#

TableWhat a row isKey fields
emailOne ask (or follow-up) to one personstatus (draft | incomplete | approved | sending | sent | replied | rejected | failed) · subject · body · review_flags · evidence · address_confidence · quality · expires_at · gmail_thread_id
replyOne inbound message on a threademail_id · from_addr · body · sentiment · sentiment_reason
eventA call, assessment or interview from a replystarts_at · link · deadline · quote · status · calendar_event_id

Ledgers and support tables#

TablePurpose
sendledgerOne row per candidate per day: cap, used, guessed cap, guessed used. Reserved atomically before a mail leaves.
llm_callEvery model call: alias, model, purpose, tokens, cost in USD and INR, latency, success.
extraction_write / extraction_rejectEvery value guardrails accepted (with provenance) or refused (with the reason).
channel_hourSuccess and failure per channel per hour — feeds the Scraping health page.
research_cacheSearch and page-read results, 24-hour TTL.
email_proposal · email_patternHow an email would be written (never sent) · approved templates.
candidatePer-person identity, mailbox and cap — two candidates, one pipeline.
settingRuntime state: last tick per kind, cursors, referral claims, the pause flag, audit history.
companydecisionThe brain’s latest answer per company: chance of an offer and its 80% band, the five stage chances, verdict, pool, shortlist rank, and the evidence (why).
dailypickOne company a day: the pick, its action (ask or fill), status, and the chances frozen at pick time for calibration.
decisionoutcomeWhat really happened at a company — replied, referred, interview, offer, rejected, no response.
mixerscrapeOne paid lookup by the mixer: source, found, matched, saved, cost, cache hit, the culture rating it carried, and why it stopped.
upskillitemYour progress on one gap: status, proof link, your line, when it went into the profile.
kg_node · kg_edge · kg_ftsThe knowledge graph and its full-text index.

Schema changes#

There are no migration files. When a model declares a column the file lacks, ensure_columns() adds it with ALTER TABLE … ADD COLUMN at startup — so pulling new code is enough.

Never let git touch the live database
jobhunter.db is git-ignored for a reason: an autostash once wiped running writes. Back it up by copying to data/backups/ (also ignored), never by committing it.