Skip to main content

Identity Graph

The identity graph turns anonymous visitor sessions into resolved, enriched people — the backbone of leads, cross-session context, and CRM sync. It lives primarily in backend, with ms-ai consuming it (via a gRPC client) for AI context.

The model

EntityMeaning
UserSessionModelone raw visitor session (device, fingerprint, IPs, URLs)
PersonModela resolved distinct visitor (may span many sessions/devices)
PersonProfileModelenriched identity for a Person (name, email, company, B2B data)
PersonIpSignalModelIP→person signals used for IP-assisted resolution

Person is the identity; PersonProfile is what we know about them. A session resolves to a Person; multiple sessions/devices can collapse into one Person as evidence accumulates.

The resolution chain

Wired in backend/app/hooks.js boot() as an ordered subsystem:

IdentityGraphRenderer → IdentityGraphConfig → SubscriptionGate
→ IdentityGraph → Verifier → PersonResolver → PersonWatch
  • IdentityGraphConfig — per-company verification policy (permissive/moderate/strict/regulated) controlling how aggressively sessions are merged.
  • SubscriptionGate — gates identity features by plan.
  • Verifier — applies the policy to candidate matches.
  • PersonResolver — the core resolution: match a session to an existing Person or create a new one, using fingerprint, IP signals, and form captures.
  • PersonWatch — drives the Nox "visitor watch" / auto-engage-on-return behavior.

Trust model (post-overhaul, 2026-07)

user_sessions.identity_trust is anonymous | claimed | verified. Provenance is enforced by source-set helpers in PersonResolverService.js, not a constants file:

  • _isHighTrustSource()form, crm, inbound_call, booking, chat_form. Only these may overwrite canonical name/email/phone (_mayOverwriteCanon); everything else fills empty fields only.
  • GENUINE_USER_SOURCES (in _decideTrust) — high-trust ∪ chat, call. Only genuine sources can verify; a first profile minted by a non-genuine source stays unverified (first_profile_untrusted_source). Enrichment can never mint verified or overwrite a self-provided name.
  • Direct-trigger reasons: first_party_form, first_party_booking (bookings auto-verify), crm_tracked_landing, caller_id_match, and chat/call self-identification → verify_chat / reason chat_identification.
  • Name moderationapp/lib/NameModeration.js screen(name) (obscenity + denylists) rejects junk/profane names before persisting; the frontend falls back to "Unknown Visitor" (getSessionName, frontend/src/common/helpers/session.js).
  • AI prompts get verified names only: IdentityGraphRendererService emits a name only when name_addressable (verified). The Apollo colleague list (company_contacts) is excluded from the default section set — opt-in via identity_graph_config.enabled_sections.

Resolution order on session create

UserSessionController.create resolves internally before spending enrichment quota: _applyWidgetUserSessionId()_backfillPersonFromPriorVisit() (widget-id/cookie link at claimed) → personResolver.wouldClaimDevice() (device-signature match at claimed). Snitcher/Apollo only fire when !internally_resolved && user_ip && suspected_bot !== true — returning identified visitors don't burn quota. When the client's own IP lookup is ad-blocked, the server fills user_ip from x-forwarded-for / x-real-ip.

Suspected identity — cross-device guesses are shadow markers, not a trust tier: ip_shadow_person_id / device_shadow_person_id on UserSessionModel. IdentityGraphService._buildSuspectedIdentity renders an owner-facing "⚠ Suspected Identity (UNCONFIRMED)" block; the frontend shows an amber SuspectedIdentityBadge on live/recorded session rows. Session display naming (getSessionName) tiers: person → self-provided → suspected → Apollo person → Snitcher company (business type) → email → Snitcher domain (non-business, ⚠).

Signals feeding resolution

  • Fingerprint (FingerprintJS) + device/browser attributes.
  • IP signals (PersonIpSignalModel, IpShadowOutcomeModel, app/lib/ipSignals.js) — IP-assisted resolution, shadow scoring, and backfill.
  • Form captures — emails/names auto-captured from forms (HubSpot/GHL/Calendly/ Typeform interception) attach identity to a session; the widget pre-chat form posts POST /api/intake with source: 'form' (direct verification trigger).
  • B2B enrichment — Snitcher (reverse IP → company) then Apollo (domain → person) fill PersonProfile. Clearbit's live API call is disabled (cache-read only); HappierLeads/RB2B remain as legacy/webhook paths. The enrichment-guess CRM push job (LeadContactSyncJob) is disabled globally (commented out in SchedulerService). Enrichment is consent-gated when the tenant enables consent gating (identification category).

Enrichment quota

UserSessionController.getSnitcherData: daily cap company.snitcher_allowed, counted in company-local timezone; 00:00–06:00 local spends only yesterday's leftover; bots never spend quota. Apollo has no separate quota — it only runs after Snitcher resolves a domain. Apollo error responses are never cached (error-cache poisoning fix); POST /migrations/cleanup-apollo-records (and the admin wrapper POST /admin/identity/maintenance/cleanup-apollo-records) purges poisoned ApolloRecordModel rows so domains retry.

Admin surface

Super-admins manage identity via frontend/src/repositories/admin/identity.js against backend's adminIdentityRoutes.js: list persons/profiles/form- submissions, session detail (fingerprint/verified-signal display), the identity graph config (moved admin-side: GET/PUT /admin/identity/config/defaults, PUT /admin/identity/config/company/:id), maintenance panels (trim session events — NDJSON progress, retry poisoned Apollo domains, unlink enrichment identities, backfill chat human-message flag, IP-signal backfills), and the GHL reconcile-conflicts queue.

Consumers

  • AI agents — cross-session context is injected into chat/voice prompts so the AI knows a returning visitor.
  • Nox — visitor lookups, presence, and watch directives.
  • CRM sync — resolved identities + outcomes flow to GHL/HubSpot. See CRM Integration.

Reliability

Identity resolution has had a dedicated reliability effort (IP-assisted resolution, form-intercept fixes, calls/read-gate/reconcile work). Treat resolution changes as high-blast-radius: they affect lead counts, dedup, and CRM records. Validate against real session samples before shipping.