Ingestion Pipeline · PostgreSQL Schema · Complete Data Mapping · Automatic Activity Tracking
2026-07-16
Every one of the 36 business fields you listed is preserved exactly as named and is retrievable for every lead from a single Lead-360 record. Nothing below removes, renames, or replaces any of them. This document adds recommended columns, the standardized ingestion pipeline, the physical schema, a field-by-field data map, and the automatic activity-tracking mechanism — all additive to the running ULDA v6.1 platform and implementable with zero downtime.
The core principle (established in the architecture report): fields are stored normalized across four domains, then assembled into one flat Lead-360 row by the Serving layer. You never lose the “single lead view” — it is produced by a serving object, not a base table.
Your 36 fields cover identity, source, enrichment, journey, and deal. For a telecom lead-gen / sales / AI / analytics platform, I recommend adding the following, grouped by purpose. All are optional and additive.
| Column | Why |
|---|---|
dnc_flag (per number) |
Federal/state DNC suppression before dial/text |
litigator_flag |
Known TCPA litigators — hard suppress |
consent_status, consent_channel,
consent_at |
Provable express consent per channel |
timezone |
Quiet-hours calling law (derive from state/ZIP) |
opt_out_sms, opt_out_email |
STOP / unsubscribe honoring |
| Column | Why |
|---|---|
phone_type (mobile/landline/voip) |
Dial strategy + mobile-only SMS |
phone_carrier, line_valid (IPQS) |
Deliverability, fraud, portability |
phone_tier (T1–T4 best-number rank) |
Which of many numbers to dial first |
email_status (valid/catch-all/invalid),
email_deliverable |
Bounce avoidance |
| Column | Why |
|---|---|
fiber_available_att / _frontier / _kinetic / _spectrum / _brightspeed |
Per-carrier yes/no (your named carriers) |
max_download_mbps, technology
(fiber/copper/FW) |
Plan match |
incumbent_carrier, address_type
(SFU/MDU/business) |
Competitive + install feasibility |
coverage_confidence,
serviceability_checked_at |
Trust + freshness |
county, census_tract, lat,
lng |
Coverage joins, territory, routing |
| Column | Why |
|---|---|
lead_score, lead_tier |
Prioritization (live) |
intent_signals (jsonb),
propensity_to_convert |
ML-ready features |
stage, status,
disposition |
Pipeline state |
owner_agent_id, team_id,
territory_id |
Assignment (distinct from “agents interacted with”) |
next_follow_up_at, attempts_per_channel
(jsonb), last_attempt_at |
Cadence management |
| Column | Why |
|---|---|
mrr, term_months,
install_date, order_status |
Revenue + fulfilment |
sub_dealer, poe_id,
commission |
FiberVolt/commission reconciliation |
| Column | Why |
|---|---|
source_count, merged_from |
Dedup transparency |
first_seen_at, updated_at,
data_quality_score, completeness |
Governance & freshness |
last_reply_at, total_replies,
open_count, last_touched_by |
Engagement signals |
Goal: every lead — from Apollo, brokers, CSV, APIs, CRM, or any future source — enters through one standardized process. No source writes to the master directly.
flowchart LR
subgraph SRC[Any source]
A[Apollo]; B[Broker CSV]; C[API]; D[CRM]; E[Future source]
end
A & B & C & D & E --> MAP[1 · Source Adapter<br/>map to canonical schema]
MAP --> LAND[2 · Landing / Bronze<br/>append-only, source-stamped]
LAND --> VAL[3 · Validation<br/>type · format · required]
VAL -->|fail| Q[(quarantine + dq_finding)]
VAL -->|pass| CLN[4 · Cleansing / Normalize<br/>geo-guard · phone E.164 · email · names]
CLN --> IR[5 · Identity Resolution<br/>deterministic → probabilistic]
IR -->|borderline| MR[(match_review)]
IR --> ENR[6 · Enrichment waterfall<br/>LeadMagic · IPQS · Apollo · SARA]
ENR --> STO[7 · Storage<br/>upsert master + append events + crosswalk]
STO --> REF[8 · Serving refresh<br/>REFRESH MV CONCURRENTLY]
REF --> L360[(Lead-360)]
upsertLeads chokepoint into a formal
contract.)ulda_stage.lead_inbox, stamped with
source_system, source_id, source_type, batch_id, ingested_at, row_hash.
Immutable audit of exactly what arrived.ulda_gov.dq_finding + quarantine (never silently
dropped).row_hash.master_lead_id; else
probabilistic (trigram + metaphone + pgvector) → borderline to
match_review; else mint a new immutable ID.enrichment
event.interaction,
enrichment, order) + write
source_crosswalk. Dual-write trigger →
change_outbox → drain.REFRESH MATERIALIZED VIEW CONCURRENTLY on
mv_lead_360, journey, score → Lead-360 reflects the new
lead.All sources call one function —
ulda_stage.ingest_batch(source_config, rows) — which runs
stages 2–8. This is the single standardized process: consistent
validation, dedup, resolution, enrichment, and storage for every lead,
forever.
leads_db
├── ulda_stage — landing / Bronze + ingestion functions (NEW)
├── ulda_master — golden entities (Master Data)
├── ulda_events — append-only events (partitioned)
├── ulda_ref — reference (serviceability)
├── ulda_serve — Lead-360, journey, score, embeddings (MV + views)
└── ulda_gov — DQ, consent, lineage, match-review, audit
-- LANDING (Bronze) — every raw row, immutable
CREATE TABLE ulda_stage.lead_inbox (
inbox_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
source_system text NOT NULL, source_id text, source_type text,
batch_id uuid NOT NULL, row_hash text NOT NULL,
raw jsonb NOT NULL, -- exactly what arrived
status text NOT NULL DEFAULT 'received', -- received|validated|resolved|stored|quarantined
ingested_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (source_system, row_hash) -- idempotent re-ingest
);
-- INTERACTION — append-only, monthly partitions (auto activity capture target)
CREATE TABLE ulda_master.interaction (
interaction_id bigint GENERATED BY DEFAULT AS IDENTITY,
master_lead_id uuid NOT NULL REFERENCES ulda_master.identity_registry(master_lead_id),
channel_id int REFERENCES ulda_master.channel(channel_id),
agent_id uuid REFERENCES ulda_master.agent(agent_id),
campaign_id text REFERENCES ulda_master.campaign(campaign_id),
direction text, interaction_type text, disposition text,
agent_login text, software_type text, -- dialer|warroom|sms_platform
occurred_at timestamptz NOT NULL, metadata jsonb,
PRIMARY KEY (interaction_id, occurred_at)
) PARTITION BY RANGE (occurred_at);
-- ENRICHMENT — append-only history (last-enrichment derived from it)
CREATE TABLE ulda_master.enrichment (
enrichment_id bigint GENERATED BY DEFAULT AS IDENTITY,
master_lead_id uuid NOT NULL REFERENCES ulda_master.identity_registry(master_lead_id),
enrichment_type text NOT NULL, provider text, source_type text, status text,
occurred_at timestamptz NOT NULL, detail jsonb,
PRIMARY KEY (enrichment_id, occurred_at)
) PARTITION BY RANGE (occurred_at);
| Table | Index | Purpose |
|---|---|---|
| interaction | (master_lead_id, occurred_at DESC) |
per-lead journey + last |
| interaction | (channel_id, occurred_at); BRIN
(occurred_at) |
channel analytics; cheap time scans |
| enrichment | (master_lead_id, occurred_at DESC) |
last-enrichment derive |
| source_crosswalk | (source_system, source_id) UNIQUE |
dedup + source lookup |
| address_serviceability | (address_id),
(carrier, fiber_available) |
fiber lookups |
| lead_inbox | (status), (batch_id) |
pipeline progress |
| person/company | GIN trigram on names | fuzzy match/search |
uuid for
entities, bigint identity+time for events).identity_registry(master_lead_id); dimensions
enforced.(source_system, source_id)
crosswalk; (address_id, carrier, checked_at)
serviceability; (source_system, row_hash) inbox.ON DELETE RESTRICT; merges via
merged_into).mv_lead_360 (core profile),
mv_lead_master_flat (all 36 fields +
additions), v_lead_journey_flat (channel pivot),
mv_lead_score, mv_analytics_*,
lead_embedding (pgvector). All refreshed
CONCURRENTLY.
Legend — Domain: M=Master · E=Event · R=Reference · S=Serving-derived.
| Business Field | Domain | Originates from | Stored in (physical) | Updated by | Lead-360 exposure |
|---|---|---|---|---|---|
| Unique Lead ID | M | Identity resolution | identity_registry.master_lead_id |
minted once, immutable | master_lead_id |
| First and Last Name | M | source / enrichment | person.first_name/last_name |
survivorship on upsert | first_name,last_name |
| M | source / enrichment | contact_point (email) → golden |
survivorship | email |
|
| Direct Number | M | source / enrichment | contact_point (phone, personal) |
survivorship | phone |
| Company | M | source / enrichment | company.legal_name (via employment) |
survivorship | company |
| Street | M | source / geo-recovery | address.line1 |
geo-guard + survivorship | street |
| City | M | source | address.city |
geo-guard | city |
| State | M | source | address.state (USPS) |
geo-guard normalize | state |
| ZIP | M | source / recovery | address.postal_code |
geo-guard | zip |
| Fiber Availability (AT&T, Kinetic, etc) | R | SARA/Kinetic/public API | address_serviceability(carrier, fiber_available) |
enrichment (serviceability) | per-carrier fiber cols |
| Company Mobile / Phone / Landline Number | M | source / enrichment | contact_point (typed phones) |
survivorship | company phone cols |
| Source ID | M | source | source_crosswalk.source_id |
on ingest | source_id |
| Source Name (Apollo, Tanveer) | M | source | source_crosswalk.source_system |
on ingest | source_name |
| Source Type (Database, Private Broker) | M | source adapter | source_crosswalk.source_type |
on ingest | source_type |
| Date Added | M | ingest | identity_registry.created_at |
set once | date_added |
| Last Enrichment Date | E→S | enrichment events | enrichment.occurred_at |
append; derived MAX | last_enrichment_date |
| Last Enrichment Type | E→S | enrichment events | enrichment.enrichment_type |
latest row | last_enrichment_type |
| Last Enrichment Source | E→S | enrichment events | enrichment.provider |
latest row | last_enrichment_source |
| First Channel Interaction | E→S | interaction | interaction (rn=1) |
append; pivot | ch1 |
| First Channel initial activity date | E→S | interaction | interaction.occurred_at (rn=1) |
pivot | ch1_at |
| First Channel Campaign Name | E→S | interaction⋈campaign | campaign.name (rn=1) |
pivot | ch1_campaign |
| Second Channel Interaction/date/campaign | E→S | interaction (rn=2) | interaction/campaign |
pivot | ch2* |
| Third Channel Interaction/date/campaign | E→S | interaction (rn=3) | interaction/campaign |
pivot | ch3* |
| Fourth Channel Interaction/date/campaign | E→S | interaction (rn=4) | interaction/campaign |
pivot | ch4* |
| Last interaction date | E→S | interaction | interaction.occurred_at |
derived MAX | last_interaction_at |
| Last interaction channel | E→S | interaction⋈channel | channel.name (latest) |
derived latest | last_channel |
| Agents interacted with (chrono, login/software) | E→S | interaction | interaction.agent_id, agent_login, software_type |
chronological array_agg | agents_interacted |
| Last Agent interaction | E→S | interaction | interaction.agent_id (latest) |
derived latest | last_agent |
| Product offered | E→S | opportunity/order | product.name via
opportunity/order_line |
derived latest | product_offered |
| Order details | E→S | order | order+order_line |
derived latest/json | order_details |
Reading: M fields return immediately (identity/geo);
R fills after a serviceability check; E→S fills as activity is captured.
Every field lands in one row:
ulda_serve.mv_lead_master_flat.
Goal: every interaction (channel, campaign, software, agent, timestamp, disposition) is captured automatically and reflected in the journey + Lead-360 — no manual logging.
flowchart LR
subgraph CH[Channels emit events]
S1[SMS platform]; S2[Dialer]; S3[Email/Instantly]; S4[Warroom/D2D app]
end
S1 & S2 & S3 & S4 -->|webhook / trigger| API[ingest_interaction API]
API --> INT[(interaction — append)]
INT --> OUT[change_outbox → drain]
OUT --> MV[REFRESH mv_lead_360 · journey CONCURRENTLY]
MV --> L360[(Lead-360 journey updated)]
Each platform calls
ulda_stage.ingest_interaction(payload) on
every event: - SMS platform → on send/reply:
channel=SMS, software_type=sms_platform, campaign, disposition
- Dialer → on call/disposition:
channel=Voice, software_type=dialer, agent_login, disposition
- Email/Instantly → on send/open/reply:
channel=Email, software_type=instantly, campaign -
Warroom / D2D → on knock/booking:
channel=Door/Web, software_type=warroom, agent_login
The function resolves the lead to its master_lead_id,
appends one interaction row (with agent login + software
type — satisfying “agents interacted with, chronological via agent login
/ software type”), and the drain refreshes the journey.
First/Second/Third/Fourth/Last channel and agent columns update
themselves.
Because the journey columns are derived from
interaction, a single append instantly changes “last
interaction”, “last agent”, and — once it’s the 1st–4th touch — the
corresponding channel slot. No column to update, no app logic to
maintain: capture the event once, the Lead-360 view recomputes.
flowchart TB
ID[identity_registry] --> P[person]; ID --> X[source_crosswalk]
P --> AECp[contact_point]; P --> AR[person_address_residency] --> ADDR[address]
ADDR --> SERV[address_serviceability]
ID --> INT[interaction]; ID --> ENR[enrichment]; ID --> OPP[opportunity] --> ORD[order] --> OL[order_line] --> PROD[product]
INT --> CH[channel]; INT --> AG[agent]; INT --> CMP[campaign]
P & X & AECp & ADDR & SERV --> CORE[mv_lead_360<br/>identity · geo · source]
INT --> JRN[v_lead_journey_flat<br/>1st–4th + last]
ENR --> LE[last-enrichment]
ORD & PROD --> LO[product + order]
SERV --> FB[fiber per carrier]
CORE & JRN & LE & LO & FB --> FLAT[mv_lead_master_flat<br/>ALL 36 fields + additions · ONE ROW]
FLAT --> APP[CRM · dialer · AI · BI]
mv_lead_master_flat LEFT JOINs the core profile to the
derived journey/enrichment/order/fiber pieces on
master_lead_id. Because every domain shares that one key,
the assembly is a clean set of joins — defined once. Applications run
SELECT * FROM ulda_serve.mv_lead_master_flat WHERE master_lead_id = $1
and receive the complete, single Lead-360 record with all business
fields intact.
See the companion Enterprise Architecture Report
(enterprise-architecture.html) for the full 7-diagram set
(System, Physical, Logical, Domain, ERD, Data Flow, Lead-360) and
per-domain/per-component detail.
| Step | Action | Risk |
|---|---|---|
| 1 | Create ulda_stage + lead_inbox +
ingest_batch() / ingest_interaction() |
new objects — none |
| 2 | Convert interaction/enrichment to
partitioned append-only (empty now) |
pure DDL — none |
| 3 | Add recommended columns (§1) to the appropriate domain tables | ADD COLUMN — instant |
| 4 | Build v_lead_journey_flat +
mv_lead_master_flat (all 36 + additions) |
derived views — none |
| 5 | Wire channel webhooks/triggers → ingest_interaction()
(auto tracking) |
app config — reversible |
| 6 | Backfill interaction from
disposition_event + warroom activity |
read-only source; batched |
Steps 1–4 are pure additive DDL/views (zero-downtime, reversible by
DROP). Steps 5–6 populate the timeline. Every business
field remains present and correctly named throughout.