Proprietary data · Architecture · People intel

People Intelligence Database

The shared people intelligence layer: one global, org-free record per person, keyed on LinkedIn URL because employers are time-bound facts, not identity. Headhunter research and Hunter email lookups read it first and write back with provenance. A job change closes an employment edge, kills its work email, and becomes the highest-value signal we have. Erasure is first-class: hard delete plus a hashed suppression list.

Status DecidedIdentity linkedin_url · never employerv1 Cache + write-backPrivacy Public facts + work emails · erasure
Headhunter runIntel lookupHit: serve $0·Miss: research once, write back
00

Two layers, one seam

Today every org pays full research price for every candidate, even one another org researched yesterday: crm_people_v2 and workflow_run_people are org-scoped, so knowledge never crosses tenants. Same split as the company layer: a global intelligence layer that improves with network effects, and the tenant-private CRM that never leaks into it. The person-shaped differences: identity that survives job changes, employment as append-only edges, and a GDPR erasure path.

Global · shared · no org_id

People intelligence

A flat canonical profile with per-tier freshness stamps, append-only employment edges carrying work emails, and erasure tombstones. Provenance reuses the shared intel_sources lineage and people events land in the unified intel_signals store. Written by headhunter runs, scrapers and agents. Read by every tenant's runs.

Tenant · private · RLS

CRM workflow

Ownership, lists, stages, notes, shortlists, outreach and run snapshots. Each org keeps its own copy of a person; intel only pre-fills the enrichment fields.

Research per person
across all tenants
$0
Cost of a cache hit
vs ~$0.01-0.10 per chain
3
intel_person_* tables
profile · edges · suppressions · sources + signals shared
3
Rollout phases
cache → acquisition → consumers
01

System design

Two interactive entity diagrams. Production today is the org-scoped world: crm_people_v2 and workflow_run_people live per tenant, employer is a free-text column, and every org re-researches the same person and re-pays Hunter. The intel layer design adds a global, org-free intel_person_* layer keyed on LinkedIn URL, with employment as append-only edges that join the company layer by domain. Toggle the two, then click any table to trace its relationships and columns.

Global people intel layerno org_id · RLS deny-all · service role only
Tenant CRMorg_id · RLS per org
Research + email providerspaid · on miss only

The new design: a global people intel layer keyed on LinkedIn URL, employment as append-only edges, joined to the company layer by domain. Hover or click a table.

WHYThe edge table is the structural change

Production stores employer as a free-text column and the work history as a JSONB blob - impossible to diff for a move and impossible to join to the company layer. The new intel_person_employments edge fixes both: a job change appends an edge, the work email dies with the edge it sat on, and company_domain joins straight into intel_companies. Same person facts, a join and a signal source the old shape could not express.

02

Data flow

How a single request moves through the new design. Pick a path - a cache hit, the people-specific job-change partial hit, or a full miss - and the diagram lights the nodes and edges it touches, in order. Click any node for detail.

1234
Cache hit

Fresh hit: the lookup finds the person by LinkedIn URL with a current employment edge and a verified work email inside TTL, served straight into the run. No Exa, Firecrawl or Hunter call.

Click any node for detail.
WHYA job change is a partial hit, not a cache failure

The middle path is what makes the people layer different. A move keeps the profile a hit and re-runs only Hunter, at the new employer domain - the cheapest possible refresh. The same write-back that heals the cache also mints the highest-value signal we have, because a new employment edge appearing is the job-change event. The pre-fill seam stays one-way: nothing tenant-derived travels back up.

03

Read-through cache

Same mechanic as the company layer, with one person-shaped twist: the middle scenario. A job change is not a cache failure - it is a partial hit that re-finds only the email and mints the best signal we have. Pick a scenario and watch the lookup path.

$0provider spend
0provider calls
cacheserved as-is
WHYThe research chain runs at most once per person

Today the Exa → Firecrawl → Hunter chain runs per org, per run, and Hunter results are cached only within a single run. With the shared cache in front, a person researched by any tenant is free for every other tenant - and the email cache key is the employment edge, so a stale email can never outlive the job that made it valid.

04

Privacy boundary and erasure

Person data is personal data, so the seam is enforced three times: by policy (only publicly observable professional facts plus discovered work emails enter the shared layer), by mechanism (client roles get RLS deny-all on intel_person_*; the ac-python-api service role is the only door), and by erasure (hard delete plus a hashed suppression list that blocks re-ingestion).

Global intel layerno org_idRLS deny-all · service role only
intel_peopleintel_person_employmentsintel_person_suppressionsintel_sourcesintel_signals
Public professional facts and verified work emails served to runs

Profiles, employment edges, emails and signals flow down into headhunter results and tenant CRM copies.

Enrichment write-back, public facts plus discovered emails

Research and Hunter output discovered during a run flows up with provenance, after the suppression check.

CRM stage, owner, notes, shortlists, outreach

Tenant workflow state, candidate ratings and who-targeted-whom never enter the shared layer.

Tenant layerorg_id + RLS per org
crm_people_v2workflow_run_peoplenotes · shortlists · outreach
Crosses the seam
  • Name, public title, employer history, location
  • Skills, languages, education, certifications
  • Public posts, speaking, publicly stated mandates
  • Discovered work emails with confidence and verified-at
  • Avatar and public social URLs
Never crosses
  • CRM stage, owner, notes, tasks, outreach history
  • Candidate ratings, shortlists, rejection reasons
  • Whether any tenant targeted, contacted or hired a person
  • Personal phone numbers sourced from tenant interactions
  • Anything derived from tenant activity, even aggregated
Erasure path · right to be forgotten
1

Erasure request received

A person (or a tenant relaying one) asks for removal. The intel_sources rows this person references (via intel_people.source_ids, the employment edges and their signals) are the data subject access inventory - exactly what we hold and where it came from.

2

Hard delete across the shared layer

Profile, employment edges, the person's intel_signals rows and the intel_sources payloads they reference are deleted in one transaction. Tenant CRM copies are each tenant's own controller responsibility.

3

Suppression hash appended

SHA-256 of the normalized LinkedIn URL and any known emails lands in intel_person_suppressions. No plaintext is retained.

4

Re-ingestion blocked at write time

Every write-back path checks the suppression table first. An erased person stays erased even when a future run rediscovers them.

LEGALTwo gates before cross-org emails ship

A legitimate-interest assessment and privacy policy update cover the GDPR basis for sharing professional data cross-tenant, and Hunter's ToS must be cleared for caching found emails across tenants. Neither blocks the schema work, but both gate cross-org emails shipping.

05

intel_person_* schema

Three person-specific tables, slimmed to match the company layer: a flat intel_people profile carrying per-tier freshness stamps (no facts table until multi-source conflicts appear), the append-only intel_person_employments edges, and the GDPR intel_person_suppressions tombstones. Provenance reuses the shared intel_sources table and people events land in the unified intel_signals store, both designed on the signals page. The structural divergence from the company layer is intel_person_employments: employer, title and work email are edges in time, not profile columns - and company_domain on the edge is where the two intel layers join. Click a table.

intel_peopleCanonical flat profile - who the person is, independent of any employer. Per-tier freshness stamps live on the row, so there is no facts table to assemble on read
linkedin_urltext · uniquenormalized, primary identity key
full_name · avatar_url · summarytext
location · country · country_codetext
skills · languagestext[]
education_history · certificationsjsonbslow-moving, cold tier
twitter_url · personal_websitetext
current_title · current_company_domaintextderived from the latest open employment edge, never written directly
extrajsonblong-tail provider fields until usage earns a column
source_idsuuid[]fk into the shared intel_sources lineage table - the same provenance table the company and signals layers write
fetched_hot_at · fetched_warm_at · fetched_cold_attimestamptzper-tier freshness stamps drive the TTL check - no facts table needed
first_seen_at · last_enriched_attimestamptz
worked example

Jane Doe leaves Acme for Beacon. The schema made tangible. Her profile row is untouched - identity survives the move. The old employment edge is closed, a new one appended, and the Acme work email dies with the edge it sat on. Only Hunter re-runs, at the new domain, so there is one new intel_sources row. The move itself becomes one append-only intel_signals row that rolls up to Beacon's company feed. Follow the keys down the cards: per_5c the person, src_b2 the new fetch, cmp_beacon the rollup.

intel_peoplethe person, unchanged by the move - only the derived current_* fields flip
idPKper_5c
linkedin_urlUNIQUElinkedin.com/in/jane-doe
full_nameJane Doe
current_titleVP Growth (derived)
current_company_domainbeacon.io (derived)
source_idsFK[src_a1, src_b2]
fetched_warm_at2026-06-10
intel_person_employmentsthe old job - closed, not deleted. The Acme work email dies with it
person_idFKper_5c
company_domainFKacme.com
titleGrowth Lead
started_at · ended_at2023-01 · 2026-06-09
work_emailjane@acme.com (superseded)
intel_person_employmentsthe new job - appended, never an UPDATE. ended_at null = current
person_idFKper_5c
company_domainFKbeacon.io
titleVP Growth
started_at · ended_at2026-06-09 · null
work_email · email_verified_atjane@beacon.io · 2026-06-10
source_idFKsrc_b2
intel_sourcesshared lineage - only Hunter re-ran, at the new domain. Profile stayed a hit
idPKsrc_b2
providerhunter
refbeacon.io
cost_usd0.05
fetched_at2026-06-10
intel_signalsunified store - ONE append-only row. related_company_id rolls it onto Beacon's company feed
idPKsig_9d4
subject_type · subject_idFKperson · per_5c
related_company_idFKcmp_beacon
signal_typejob_change
payload{ from: Acme, to: Beacon, title: VP Growth }
source_idFKsrc_b2
observed_at2026-06-09
PMThe edge table is the product

Putting the work email on the employment edge encodes the invalidation rule in the schema itself: close the edge and the email is superseded with it, no cleanup job needed. And a new edge appearing is precisely the job-change signal phase 2 will score - the cache and the signal pipeline share one data model.

06

Decision register

The six decisions the earlier workspace left open, now closed. Each tab shows the call and why it went that way.

schema

What identifies a person?

decided

Normalized LinkedIn URL is the canonical key - never the employer

1

Exact normalized LinkedIn URL (lowercase, canonical slug) auto

2

Exact email match against any employment edge auto

3

Fuzzy name + employer suggest only

WHYIdentity must survive a job change

Emails churn when people move and employer is a time-bound fact, so neither can anchor identity. The LinkedIn URL is the one key that follows the person. Email still matches automatically because an edge email is unambiguous - but fuzzy name + employer never merges on its own; in a shared layer a wrong merge corrupts every tenant at once.

The paired company-side design lives at Company intelligence database - the two layers share the storage pattern and privacy mechanism, and join on company_domain. The shared intel_sources lineage and the unified intel_signals store are designed on the Signals intelligence database.
AgencyCore · People Intelligence DatabasePeople intel · decided architecture