Skip to main content

Identity Graph Overview

Audience: All teams (sales, management, QA, support, engineering) · Where in app: Admin → Identity (super-admin only); runs invisibly behind every chat/call · Plan availability: All plans (gated by an active subscription; super-admin can disable per tenant)

The Identity Graph stitches a visitor's identity together across visits so the AI agent — and the admin — sees a returning visitor as a known person, not a fresh anonymous session each time. Without it, every visit starts from zero: the AI re-asks "what's your name?" and has no idea the visitor booked a demo last week. With it, the AI greets returning customers by name, references their prior questions, knows their lifecycle stage, and skips qualification questions they already answered.

It is super-admin owned: customers don't normally see any configuration. It runs behind the scenes, assembling a context block that is injected into the AI's prompt every time it talks to someone the system recognizes.

What it does

  • Recognizes returning visitors across sessions and devices, even after they clear cookies/localStorage.
  • Pulls together everything a tenant knows about that person — identity, engagement history, recent pages, past conversations, CRM state, bookings, captured forms, and AI-extracted facts.
  • Hands that context to the AI (chat agent, voice/call agent, outbound campaign agent) so responses are personalized and qualification isn't repeated.
  • Maintains strict per-tenant isolation — Company A's view of a person never leaks to Company B, even though both recognize the same human.
  • Captures website forms the visitor submits (name/email/phone/company) and uses them as a strong identity signal.
  • Powers admin/Nox lookups — "what did John do this week?" answered across sessions.

Why it matters (sales/management framing)

  • The AI doesn't ask a returning customer to re-introduce themselves.
  • The AI knows the visitor already booked a demo and can reference it.
  • The AI knows the visitor objected to pricing last time and can pre-empt it.
  • Resolved identities flow to the CRM (HubSpot/GHL) as real contacts, and outcomes flow back. See CRM Integration.

How it works

The graph is built from three core records (full detail in Person vs Profile):

  • Person — one global row per real human, matched across tenants by hashed email/phone (match_keys). Tenant-agnostic; holds only basic identity.
  • PersonProfile — one row per (Person × Company). This is where everything a single tenant knows about a person lives. Strictly sandboxed per company.
  • UserSession — a single visit. It belongs to a Person (once resolved) and a Company.

A visit flows through an ordered resolution chain (wired in backend/app/hooks.js boot()):

IdentityGraphRenderer → IdentityGraphConfig → SubscriptionGate
→ IdentityGraph → Verifier → PersonResolver → PersonWatch
  1. Config & subscription gate — load the effective config for the tenant; bail if the tenant has no active subscription.
  2. PersonResolver — match the session to an existing Person (by email/phone hash, CRM contact id, returning device, or anchor-on-session), or create one.
  3. Verifier — decide whether the session is verified or merely claimed (see below).
  4. Renderer — assemble the cross-session context block and inject it into the AI prompt (only for verified sessions).
  5. PersonWatch — fire any standing "notify me when X returns" directives.

The verification gate

The graph surfaces cross-session data only for verified sessions. An unverified ("claimed") session — someone who typed an email into chat but whom the system doesn't yet trust — sees only the current visit. The verification policy decides how strict the bar is:

PolicyScore thresholdUse case
permissiveAny signal verifiesLegacy / lowest friction
moderateScore ≥ 60Balanced
strictScore ≥ 80Default for new tenants
regulatedScore path off — direct triggers onlyHealthcare / finance

See Verification Policy for the full mechanics and signal scoring.

What verified sessions see

The AI prompt context is assembled from toggleable sections (super-admin controls which appear). The AI is given verified names only — the identity section emits a name only when the session is verified (name_addressable); geo prefers the visitor's IP city over the enriched company city:

  • Identity — name (verified only), email, phone, company, location
  • Company Insights — domain, revenue band, employee count (when enrichment connected)
  • Engagement — lead score, session count, pages viewed
  • Current Session — current page, traffic source
  • Recent Pages — last visited pages (deduped, cap default 5)
  • Past Conversations — last chat/call summaries (cap default 5)
  • Behavioral Signals — high-intent pages, products searched
  • Bookings — meetings, tagged [PAST] / [UPCOMING]
  • CRM State — lifecycle stage, deals, tags, notes (hourly-cached)
  • Form Responses — captured form field answers
  • Extracted Facts — AI-distilled facts (cap default 15)
  • Suspected Identity — owner-facing "⚠ Suspected Identity (UNCONFIRMED)" block from shadow markers
  • Company Contacts — Apollo colleague list; excluded from the default section set, opt-in via enabled_sections

Full detail in Cross-Session Context.

What unverified ("claimed") sessions see

  • Identity only if they typed it this visit
  • The current session only
  • Their own data captured this session

No past conversations, no CRM state, no extracted facts, no cross-session bookings.

Configuration & options

All configuration is super-admin owned under Admin → Identity. Customers have no knobs by default.

  • Global defaults live in a single identity_graph_defaults document (the IdentityGraphDefaultsModel singleton). Fields: enabled, enabled_sections[], custom_intro, max_recent_pages, max_past_conversations, max_extracted_facts, verification_policy, accept_inbound_call_verification, capture_website_forms.
  • Per-company override — set companies.identity_graph_use_custom_config = true and populate companies.identity_graph_config to override any field for one tenant. Otherwise the global defaults apply.
  • Subscription gate — independent runtime check; a trial-expired or cancelled tenant gets no graph context (cached 60s).

Full reference in Admin Controls.

For QA/support: if a returning visitor isn't recognized, check in order: (1) is the tenant's subscription active? (2) is the session verified or only claimed? (3) is the policy too strict for the signals present? (4) is the relevant section enabled globally?

Behaviors & edge cases

  • Verified vs claimed is the single most important distinction. Verified = system trusts the identity and surfaces full cross-session memory. Claimed = visitor typed something but the system won't trust it across visits yet. Since the 2026-07 trust overhaul, self-identification in chat or call verifies directly (verify_chat, reason chat_identification); only genuine user sources (form / chat form / booking / inbound call / chat / call) can verify at all — enrichment and CRM auto-creates never can (first_profile_untrusted_source).
  • Self-provided outranks enrichment. Only high-trust sources (form, CRM, inbound call, booking, chat form) may overwrite canonical name/email/phone; enrichment (Apollo, then Snitcher below it) only fills empty fields. Junk/profane names are rejected by name moderation (NameModeration.screen) and surface as "Unknown Visitor".
  • Suspected identity — cross-device IP/device-signature guesses land as shadow markers (ip_shadow_person_id / device_shadow_person_id), shown as an amber "Suspected" badge and a "Suspected Identity (Unconfirmed)" panel. Never a trust tier, never greeted by name.
  • Identity graph resolves before enrichment. On session create the graph tries widget-id and device-signature linking first; Snitcher/Apollo only fire for still-unresolved, non-bot sessions — returning identified visitors don't burn enrichment quota.
  • Same person across tenants — recognized as the same global Person, but each tenant only ever reads its own PersonProfile. No data crosses.
  • Conflict guard — once a session is verified for one Person, a later claim matching a different Person is rejected (audited), not allowed to flip the session.
  • Config changes apply going forward — already-assembled context isn't retroactively recomputed; the next conversation uses the new config.
  • Subscription cache (60s) — a freshly cancelled tenant may briefly still get graph context.

Plan & limits

The Identity Graph is available on all plans, but always subject to:

  • An active, non-expired subscription (trial-expired/cancelled → no graph context).
  • Super-admin allowing it for the tenant (identity_graph_allowed).

There is no per-seat or per-record price on the graph itself; it's part of the product. Verification strictness and which sections appear are operational settings, not plan tiers. (Plan-tier gating of specific identity features beyond the subscription check: verify with product.)

Technical implementation

  • Models: PersonModel (people), PersonProfileModel (person_profiles), PersonIpSignalModel (person_ip_signals), IdentityGraphDefaultsModel (identity_graph_defaults), plus UserSessionModel.
  • Services: IdentityGraphConfigService, IdentityGraphSubscriptionGateService, IdentityVerifierService, PersonResolverService, PersonWatchService, IdentityGraphRendererService, IdentityGhlReconcileService, IdentityCleanupService.
  • Resolution chain is wired as an ordered subsystem in backend/app/hooks.js. ms-ai consumes the assembled graph (via gRPC/HTTP) for prompt context and caches it ~30s, invalidated by the backend when identity changes.

For the engineering deep-dive — resolution chain internals, IP-assisted resolution, reliability history — see Identity Graph (subsystem).

What Nox can tell you

  • Why a visitor is or isn't verified (signal breakdown vs policy threshold)
  • What the AI saw in context on a specific session
  • A cross-session timeline for a person
  • How many visitors are verified vs claimed

Nox cannot change settings — super-admin edits directly.