Skip to main content

Multi-tenancy

A lead (person) can interact with multiple organizations on the platform. The same phone number can be a customer of Org A and unknown to Org B. Lead identity is global (one person = one Lead). All behavioral data is scoped per organization via LeadOrganization.
DataScope
Name, email, phoneGlobal — person identity
Channel identitiesGlobal — ways to reach the person
Memories (purchases, interests)Per org — bought from Org A, not from Org B
EventsPer org — Org A’s Stripe webhook
ML featuresPer org — feature vector relative to that org’s context
Org A never sees Org B’s data. The shared global identity is an internal optimization for identity resolution.

ChannelIdentity as sole source of resolution

ChannelIdentity is the sole source of identity resolution. Lead.phone and Lead.email are profile data — they are not used to resolve “who does this event belong to”. When an event arrives (e.g., WhatsApp message from +5511999999999):
1. Look up ChannelIdentity WHERE channel = "whatsapp" AND identifier = "+5511999999999"
2. Found → follow to Lead → LeadOrganization
3. Not found → create Lead + ChannelIdentity + LeadOrganization
This enables cross-channel: the same lead can have identities on WhatsApp, web chat, Instagram, etc. All point to the same Lead with the same context. When a lead is created (via API, CSV import, or automatically via event), at least one ChannelIdentity must be created alongside it so the lead is discoverable.

Data Model

Lead

The person. Exists exactly once regardless of how many organizations they interact with.
Lead
├── id              UUID, PK
├── name            VARCHAR(255), nullable
├── email           VARCHAR(255), nullable, unique where not null
├── phone           VARCHAR(50), nullable, unique where not null
├── metadata        JSONB — free-form data (origin, language, etc.)
├── created_at      TIMESTAMPTZ
└── updated_at      TIMESTAMPTZ
email and phone are profile data. Identity resolution is done exclusively via ChannelIdentity.

ChannelIdentity

Lead’s identity on a channel. A lead can have N identities. Sole source of identity resolution.
ChannelIdentity
├── id                  UUID, PK
├── lead_id             UUID, FK → Lead
├── channel             ENUM (whatsapp, web, instagram, email, voice, telegram, sms)
├── channel_identifier  VARCHAR(255) — phone number, email, cookie id, etc.
├── metadata            JSONB — channel-specific data
└── linked_at           TIMESTAMPTZ
UNIQUE on (channel, channel_identifier).

LeadOrganization

The relationship between a lead and an organization. All behavioral data hangs from this entity.
LeadOrganization
├── id               UUID, PK
├── organization_id  UUID — FK to Organization (IAM domain)
├── lead_id          UUID, FK → Lead
├── metadata         JSONB — tags, segment, notes, org-specific data
├── created_at       TIMESTAMPTZ
└── updated_at       TIMESTAMPTZ
UNIQUE on (organization_id, lead_id). Classifications like “is customer?”, “how engaged?”, “churn risk?” are multi-dimensional and live in features and memories, not as fixed fields. A lead can be a Whey customer and a Creatine prospect within the same org.

Entity Relationship