Person & Account Identity (Staff)¶
How Rose figures out which company a visitor is from (account identity) and who the visitor is (person identity), how the two are stored as evidence, and how the staff Person identity comparison page judges whether the signals agree.
Staff-only diagnostics. Backed by the
person_identity_comparisonview and rendered byPersonIdentityDiagnosticsPage.tsx.For how the account cascade and sources actually run, see Visitor Enrichment.
Two independent pipelines¶
| Account identity (company) | Person identity (who) | |
|---|---|---|
| Question | Which company is this visitor from? | Who is this individual? |
| Providers | browser_reveal, snitcher_radar, rb2b, enrich_so (+ first_party_email, hubspot) |
vector, rb2b_person |
| Runs as | Cascade in unified_enricher.py (stops at first match) |
Per-provider waterfall in person_identity.py + Vector webhook |
| Writes to | visitors.enrichment_data, accounts, visitor_account_identifications |
visitor_identifications |
The account cascade resolves a visitor's IP to a company and stops at the first source that matches:
Short-circuit is per enrichment run. A returning visitor (or a different visitor on the same account) can be resolved by a different source on a later run, so over time one account accumulates claims from several providers.
Evidence tables¶
Each provider claim is stored as one row, so nothing is lost when sources disagree:
visitor_account_identifications— one row per(visitor_id, provider), the company each account source claims.visitor_identifications— one row per(visitor_id, provider), the person each person source claims.
Person feeds the account list¶
When a person provider identifies someone at a company, that company joins the account evidence — the same way the Vector webhook already does. So the account cell lists every company implied by both account-side and person-side providers. The canonical selector reranks all evidence by provider priority, so a person provider can also promote its company to the visitor's main account.
Agreement: per-cell consistency¶
The comparison view compares like with like, each cell on its own key:
- Account cell lines agree on the same company domain.
- Person cell lines agree on the same person (email, falling back to name).
A single provider line cannot agree with itself, so two or more provider lines in one cell are required to judge.
Only real provider lines count. The canonical-visitor-email fallback (the
visitor's own email surfaced as the person) is not an independent source, so a
row with one account line and only that fallback is not_comparable, not
agree.
Account enrichment is account-scoped (the fallback rule)¶
accounts.enrichment_data is shared by every visitor on the account, so it is
not a per-visitor lookup. The seed backfill in
20260617184057_person_identity_waterfall_evidence.sql inherits the account's
provider into a visitor's account evidence only when the visitor has no
enrichment of its own:
Without this guard, the shared account provider was echoed onto every visitor —
manufacturing a second account line (e.g. apple.com (snitcher) echoed onto a
visitor that rb2b actually resolved) and a false agree. With it, visitors
with their own signal use only that; account-only visitors keep the inherited
line as a single, non-corroborating line.
Where things live¶
| Concern | Location |
|---|---|
| Account cascade + sources | backend/packages/ixchat/ixchat/enrichment/unified_enricher.py, source_config.py |
| Account source: rb2b (IP→company) | enrichment/rb2b_enrichment.py |
| Person waterfall (Vector skipped here; written by webhook) | enrichment/person_identity.py |
| Person→account fold | packages/ixdata/ixdata/clients/person_identity.py |
| Account evidence write + canonical selector | packages/ixdata/ixdata/clients/account_identity.py, reselect_visitor_account_identity |
| Comparison view | person_identity_comparison (migrations 20260617184057, 20260619105932) |
| Diagnostics page | frontend/client-backoffice/src/pages/PersonIdentityDiagnosticsPage.tsx |