Adding Enrichment Vendors
This guide covers the end-to-end process of adding a new third-party data vendor to the CRED platform. Adding a vendor requires changes across multiple projects.
Overview
When adding a new enrichment vendor (e.g., Apollo, Lusha, Cognism), you need to update:
| Project | Repository | Purpose |
|---|---|---|
| CRED Model | cred-model |
Database tables for storing provider data |
| DBT | cred-dbt |
DataDescription exposure and field mappings |
| Model API | cred-model-api |
API integration and data fetching |
| Commercial | cred-api-commercial |
UI integration, waterfall configuration |
Process Flow
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β CRED Model β β β DBT β β β Model API β β β Commercial β
β (Tables) β β (Schema) β β (Endpoints) β β (UI) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
Quick Start Summary
| Step | Project | Repository | What to Do | Key Files |
|---|---|---|---|---|
| 1 | CRED Model | cred-model |
Create migration + sync config | migrations/*.ts, sync-providers.config.ts |
| 2 | DBT | cred-dbt |
Add to provider tables | data_description_providers_tables.sql |
| 3 | Model API | cred-model-api |
Domain, services, adapter, factory, GraphQL | ~15 files across layers |
| 4 | Commercial | cred-api-commercial |
Enum, feature, seed, provider list, validation | 5-6 files |
Naming Conventions
Critical: Use Consistent Naming Across All Projects
All projects are interrelated. Using inconsistent names will break the integration.
| Element | Convention | Example |
|---|---|---|
| Abbreviation | UPPERCASE, no spaces | AP, COG, LUSHA, ROCKETREACH |
| Table Name (Person) | {ABBR}Person |
APPerson, CGPerson, LSPerson, RRPerson |
| Table Name (Company) | {ABBR}Company |
APCompany |
| Provider Unique ID | {abbr}Id or providerUniqueId |
apolloId, cognismId |
| Feature Constant | FEATURE_WATERFALL_DATASOURCE_{ABBR} |
FEATURE_WATERFALL_DATASOURCE_AP |
When adding a new vendor, decide on:
- Abbreviation (e.g.,
NVfor "NewVendor") - use this EVERYWHERE - Full name (e.g., "NewVendor") - for display purposes only
Cross-Reference Table:
| Project | Repository | Where Abbreviation is Used |
|---|---|---|
| CRED Model | cred-model |
Table name: NVPerson, column: nvId or providerUniqueId |
| DBT | cred-dbt |
dataSourceAbbreviation: "NV", tableName: "NVPerson" |
| Model API | cred-model-api |
ProviderDataSourceAbbreviation.NV, folder: newvendor/ |
| Commercial | cred-api-commercial |
CustomFieldRecordDataSourceEnum.NV, feature constant |
1. CRED Model (Database Tables)
Repository: cred-model
Adding a new provider table (e.g., NVPerson, NVCompany) uses a single migration that handles everything: table creation, seeding, and replication setup.
Single Migration Pattern
File: src/data/migrations/{timestamp}-create-nv-person.ts
import * as Knex from "knex";
import addSetUpdatedAtTrigger from "./utils/add-set-updated-at-trigger";
import addToReplicationSet from "./utils/add-to-replication-set";
import addToStitchSync from "./utils/add-to-stitch-sync";
import seedIfEmpty from "./utils/seed-if-empty";
export async function up(knex: Knex): Promise<void> {
// 1. Create table
await knex.schema.createTable("NVPerson", (table) => {
table.increments("id").notNullable().primary();
table.string("providerUniqueId").notNullable().unique();
table.integer("personId"); // or "companyId" for company tables
table.foreign("personId").references("Person.id").onDelete("cascade");
// Provider-specific columns...
table.string("firstName");
table.string("lastName");
table.jsonb("someJsonField");
// Standard timestamps
table.dateTime("createdAt").notNullable().defaultTo(knex.raw("NOW()"));
table.dateTime("updatedAt").notNullable().defaultTo(knex.raw("NOW()"));
table.dateTime("deletedAt");
});
await addSetUpdatedAtTrigger(knex, "NVPerson");
// 2. Insert seed row with ALL columns populated
await seedIfEmpty(
knex,
"NVPerson",
{
providerUniqueId: "seed-nv-person-001",
personId: null,
firstName: "Seed",
lastName: "Record",
someJsonField: { example: "data" },
deletedAt: null,
},
"providerUniqueId",
"seed-nv-person-001",
);
// 3. Add to pglogical replication
await addToReplicationSet(knex, "NVPerson");
// 4. Enable Stitch sync for BigQuery
await addToStitchSync("NVPerson", "updatedAt");
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable("NVPerson");
}
What Each Step Does
| Step | Utility | Purpose |
|---|---|---|
| 1 | addSetUpdatedAtTrigger() |
Auto-updates updatedAt column on row changes |
| 2 | seedIfEmpty() |
Inserts seed row if table is empty (idempotent). Seed row has ALL columns populated so DBT can detect the full schema. |
| 3 | addToReplicationSet() |
Adds table to pglogical replication set. Data (including seed row) replicates to model-api automatically. Skips in local dev where pglogical is not installed. |
| 4 | addToStitchSync() |
Enables Stitch replication via API for BigQuery sync. Configures incremental sync using the specified replication key (default: updatedAt). Skips gracefully when Stitch credentials are not configured. |
Seed Row Requirements
| Requirement | Description |
|---|---|
| ALL columns populated | Required for DBT schema detection |
| Foreign keys = null | e.g., personId: null, companyId: null |
| Recognizable seed ID | e.g., "seed-nv-person-001" |
| deletedAt = null | If soft-delete column exists |
Benefits
| Benefit | Description |
|---|---|
| Single source of truth | Migration in CRED Model creates everything |
| No seeding in model-api | Data replicates via pglogical automatically |
| Works locally | Replication step skips gracefully if pglogical not installed |
| Idempotent | Safe to run multiple times |
Add Sync Provider Configuration
File: src/data/config/sync-providers.config.ts
export const PROVIDER_TABLES: ProviderTableConfig[] = [
// ... existing providers ...
{
tableName: "NVPerson", // Must match migration table name
primaryKey: "providerUniqueId",
description: "NewVendor person data",
},
];
Required Elements
| Element | Required | Purpose |
|---|---|---|
id (auto-increment) |
β | Internal primary key |
| Provider unique ID | β | e.g., providerUniqueId - for sync |
createdAt |
β | Timestamp |
updatedAt |
β | Required for sync time filtering |
deletedAt |
β | Soft delete support |
addSetUpdatedAtTrigger() |
β | Auto-updates updatedAt on changes |
seedIfEmpty() |
β | Seeds ALL columns for DBT detection |
addToReplicationSet() |
β | Enables pglogical replication |
addToStitchSync() |
β | Enables Stitch sync for BigQuery |
| Sync config entry | β | Enables cross-environment sync |
Optional Elements
| Element | Notes |
|---|---|
personId / companyId FK |
Links to main Person or Company table |
| Indexes | Improves query performance |
What You Do NOT Need
| Not Required | Reason |
|---|---|
| β TypeORM entity files | Tables use raw Knex queries |
| β Dedicated service files | Data accessed via sync mechanism |
| β Manual SQL sync code | System auto-discovers columns |
| β Seeding in model-api | Seed row replicates via pglogical automatically |
Single Migration = Everything
One migration file handles table creation, seeding, and replication setup. Data automatically syncs to model-api via pglogical.
2. DBT (DataDescription)
Repository: cred-dbt
To expose a new enrichment source table in DataDescription, you need to add it to the provider tables configuration. The enrichment source tables are PostgreSQL tables stored in the credmodel_google schema.
Step 1: Ensure Source Table Exists
Prerequisite
The PostgreSQL table must exist in the credmodel_google schema and be defined in sources_datasets/credmodel_google.yml.
Examples of existing tables:
| Table | Description |
|---|---|
APPerson |
Apollo Person |
CGPerson |
Cognism Person |
RRPerson |
RocketReach Person |
LSPerson |
Lusha Person |
Step 2: Add Entry to Provider Tables List
File: models/credentity/data_description/intermadiate/data_description/data_description_providers_tables.sql
Add a new entry to the providerTables list:
{"tableName": "NVPerson", "parentDataName": "Person", "displayName": "NewVendor Person", "dataSourceAbbreviation": "NV"},
Parameters:
| Parameter | Description | Example |
|---|---|---|
tableName |
Exact table name from CRED Model migration | NVPerson (must match exactly!) |
parentDataName |
Top-level entity: "Person" or "Company" |
"Person" |
displayName |
Human-readable name | "NewVendor Person" |
dataSourceAbbreviation |
System abbreviation (same across projects) | "NV" |
Example - Adding a new source:
{% set providerTables = [
{"tableName": "APPerson", "parentDataName": "Person", "displayName": "Apollo Person", "dataSourceAbbreviation": "AP"},
{"tableName": "RRPerson", "parentDataName": "Person", "displayName": "RocketReach Person", "dataSourceAbbreviation": "ROCKETREACH"},
{"tableName": "CGPerson", "parentDataName": "Person", "displayName": "Cognism Person", "dataSourceAbbreviation": "COG"},
{"tableName": "LSPerson", "parentDataName": "Person", "displayName": "Lusha Person", "dataSourceAbbreviation": "LUSHA"},
{"tableName": "NVPerson", "parentDataName": "Person", "displayName": "NewVendor Person", "dataSourceAbbreviation": "NV"}, # β Add here
] %}
Step 3: Verify Integration
After adding the entry, the provider table will automatically:
- Generate an ID: Using
to_hex(md5(concat('PROVIDER-TABLE-', tableName))) - Link to Parent Entity: Via
data_description_cred_entity - Appear in DataDescription: Available for metadata queries
Step 4: Add Metadata (Optional)
If you want to add display metadata for the provider table, add an entry to the DataDescriptionMetadata seed table in BigQuery:
Location: cred-1556636033881.cred_seed.DataDescriptionMetadata
Add a row with:
id: The generated ID (calculate:to_hex(md5(concat('PROVIDER-TABLE-', 'NVPerson'))))name: Display name (e.g., "NewVendor Person")description: Description of the tabletooltip: Tooltip text
ID Generation
The ID is auto-generated from the table name. For NVPerson, the ID would be the MD5 hash of 'PROVIDER-TABLE-NVPerson'.
Testing
# Run the dbt model
dbt run --select data_description_providers_tables
# Verify the table appears in output
# Check it's linked to correct parent entity
# Verify it appears in DataDescription model
Current Provider Tables
| Table Name | Parent Entity | Display Name | Abbreviation | Notes |
|---|---|---|---|---|
APPerson |
Person | Apollo Person | AP | Also has APCompany |
RRPerson |
Person | RocketReach Person | ROCKETREACH | Person only |
CGPerson |
Person | Cognism Person | COG | Person only |
LSPerson |
Person | Lusha Person | LUSHA | Person only |
Pattern
Notice how abbreviations match: AP β APPerson table, CustomFieldRecordDataSourceEnum.AP, ProviderDataSourceAbbreviation.AP
3. Model API
Repository: cred-model-api
The Provider Enrichment system provides a unified interface for enriching entity data (Person, Company) from multiple external data providers.
Architecture Overview
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ProviderEnrichmentOrchestrator β
β (Entry point for all enrichment requests) β
βββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ProviderFactory β
β createProvider(providerAbbreviation, entityType) β
βββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββ΄ββββββββββββββββββ
βΌ βΌ
ββββββββββββββββββ ββββββββββββββββββ
β PERSON β β COMPANY β
β Adapters β β Adapters β
ββββββββββ¬ββββββββ ββββββββββ¬ββββββββ
β β
βββββββββ¬ββββββββΌββββββββ¬ββββββββ β
βΌ βΌ βΌ βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββ ββββββββββββββββββ
βApolloββLusha ββCognisββRocketβ β Apollo Company β
βPersonββPersonββPersonββReach β β Adapter β
ββββββββββββββββββββββββββββββββ ββββββββββββββββββ
Supported Providers & Entity Types
| Provider | Abbreviation | Person | Company |
|---|---|---|---|
| Apollo | AP | β | β |
| Lusha | LUSHA | β | β |
| Cognism | COG | β | β |
| RocketReach | ROCKETREACH | β | β |
Component Responsibilities
| Component | Responsibility |
|---|---|
| ProviderEnrichmentOrchestrator | Entry point, routes requests to appropriate provider |
| ProviderFactory | Creates adapter based on providerAbbreviation + entityType |
| Enrichment Adapters | Implement IProviderEnrichmentService, adapt provider to unified interface |
| Default Services | Cache-first lookup, falls back to API, persists results |
| API Services | Pure HTTP calls to external APIs, response mapping |
| Repositories | Redis caching + PostgreSQL persistence |
Data Flow
Request: enrichById(companyId=123, provider=AP, entityType=COMPANY)
β
βΌ
βββββββββββββββββββββββββββ
β ProviderFactory β
β β ApolloCompanyAdapter β
βββββββββββββ¬ββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β DefaultApolloCompany β
β Service β
βββββββββββββ¬ββββββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ
β Cache/DB Lookup βββββ HIT βββββΆβ Return cached β
β (Repository) β β data β
ββββββββββ¬βββββββββ βββββββββββββββββββ
β MISS
βΌ
βββββββββββββββββββ
β ApiApolloCompanyβ
β Service (HTTP) β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Persist to DB β
β (Repository) β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Return enriched β
β data β
βββββββββββββββββββ
Files Structure
src/
βββ domain/
β βββ {vendor}/
β β βββ entity/
β β β βββ {abbr}-person.ts
β β β βββ {abbr}-company.ts
β β βββ repository/
β β β βββ {vendor}-person-repository.ts
β β β βββ {vendor}-company-repository.ts
β β βββ service/
β β βββ {vendor}-person-service.ts
β β βββ {vendor}-company-service.ts
β βββ provider-enrichment/
β βββ entity/provider-types.ts
β βββ service/provider-enrichment-service.ts
βββ data/repos/{vendor}/
β βββ db-{vendor}-person-repository.ts
β βββ cache-{vendor}-person-repository.ts
β βββ db-{vendor}-company-repository.ts
β βββ cache-{vendor}-company-repository.ts
βββ services/
βββ {vendor}/
β βββ api-{vendor}-person-service.ts
β βββ default-{vendor}-person-service.ts
β βββ api-{vendor}-company-service.ts
β βββ default-{vendor}-company-service.ts
βββ provider-enrichment/
βββ provider-factory.ts
βββ provider-enrichment-orchestrator.ts
βββ {vendor}-enrichment-adapter.ts
Step 1: Define the Domain Layer
Create the domain entities and service interface in src/domain/newvendor/.
a) Create Entity Types
File: src/domain/newvendor/entity/nv-person.ts
import { EntityData } from "../../base/entity";
export interface NVPerson extends EntityData {
nvId: string; // Provider unique ID (matches DB column)
personId?: number | null;
name?: string;
// ... vendor-specific fields
}
b) Create Repository Interface
File: src/domain/newvendor/repository/newvendor-person-repository.ts
export default interface NewVendorPersonRepository {
lookupPerson(linkedinUrl: string): Promise<NVPersonCreateData | null>;
// ... other repository methods
}
c) Create Service Interface
File: src/domain/newvendor/service/newvendor-person-service.ts
export type NewVendorPersonInput = {
personId?: number;
linkedinUrl?: string;
cacheOnly?: boolean;
freshOnly?: boolean;
};
export type NewVendorPersonCreateData = {
personData: Omit<NVPerson, "id" | "createdAt" | "updatedAt">;
// ... nested data
};
export default interface NewVendorPersonService {
lookupPerson(
input: NewVendorPersonInput,
): Promise<NewVendorPersonCreateData | null>;
}
Step 2: Implement the Services Layer
Create services in src/services/newvendor/.
a) Create API Service (external API calls)
File: src/services/newvendor/api-newvendor-person-service.ts
export default class ApiNewVendorPersonService
extends RestService
implements NewVendorPersonService
{
constructor(private apiKey: string, private storage: CacheStorage) {}
async lookupPerson(
input: NewVendorPersonInput,
): Promise<NewVendorPersonCreateData | null> {
// Call external vendor API
// Map response to internal data structure
}
}
b) Create Default Service (orchestrates cache + API)
File: src/services/newvendor/default-newvendor-person-service.ts
export default class DefaultNewVendorPersonService
implements NewVendorPersonService
{
private static instance: DefaultNewVendorPersonService;
static getInstance(
personService: PersonService,
): DefaultNewVendorPersonService {
// Singleton pattern
}
async lookupPerson(
input: NewVendorPersonInput,
): Promise<NewVendorPersonCreateData | null> {
// Check cache first, then call API, persist results
}
}
Step 3: Create the Provider Enrichment Adapter
This is the key integration point for the unified waterfall logic.
File: src/services/provider-enrichment/newvendor-enrichment-adapter.ts
import { BaseProviderEnrichmentService } from "../../domain/provider-enrichment/service/provider-enrichment-service";
import {
ProviderDataSourceAbbreviation,
ProviderOptions,
} from "../../domain/provider-enrichment/entity/provider-types";
export class NewVendorEnrichmentAdapter extends BaseProviderEnrichmentService<
NewVendorPersonCreateData,
NVPerson
> {
constructor(
private readonly newvendorService: NewVendorPersonService,
dataDescriptionService: DataDescriptionService,
cacheStorage?: CacheStorage,
) {
super(
ProviderDataSourceAbbreviation.NV, // Must match enum value
dataDescriptionService,
cacheStorage,
);
}
protected validateOptions(_options: ProviderOptions): string | null {
// Return null if valid, error message if invalid
return null;
}
protected async lookupPersonData(
personId: number,
options: ProviderOptions,
): Promise<NewVendorPersonCreateData | null> {
return this.newvendorService.lookupPerson({
personId,
freshOnly: options.freshOnly,
});
}
protected extractPersonData(
result: NewVendorPersonCreateData,
): NVPerson | null {
return result.personData as NVPerson;
}
protected getProviderName(): string {
return "NewVendor";
}
}
Step 4: Register the Provider in the Factory
a) Add to ProviderDataSourceAbbreviation enum
File: src/domain/provider-enrichment/entity/provider-types.ts
export enum ProviderDataSourceAbbreviation {
AP = "AP",
LUSHA = "LUSHA",
COG = "COG",
ROCKETREACH = "ROCKETREACH",
NV = "NV", // β Add new vendor (must match Commercial abbreviation)
}
b) Update ProviderFactory
File: src/services/provider-enrichment/provider-factory.ts
export class ProviderFactory {
constructor(
private readonly apolloService: ApolloPersonService,
private readonly lushaService: LushaPersonService,
private readonly cognismService: CognismPersonService,
private readonly rocketreachService: RocketReachPersonService,
private readonly newvendorService: NewVendorPersonService, // β Add
public readonly dataDescriptionService: DataDescriptionService,
private readonly cacheStorage?: CacheStorage,
) {}
createProvider(
providerAbbreviation: ProviderDataSourceAbbreviation,
): IProviderEnrichmentService {
switch (providerAbbreviation) {
// ... existing cases
case ProviderDataSourceAbbreviation.NV:
return new NewVendorEnrichmentAdapter(
this.newvendorService,
this.dataDescriptionService,
this.cacheStorage,
);
default:
throw new Error(
`Unknown provider type: ${providerAbbreviation}`,
);
}
}
}
Step 5: Register in the Container Layer
a) Update types.ts
File: src/container/types.ts
export type ApiServicesInput = {
// ... existing services
newvendor?: NewVendorPersonService;
};
b) Update services.ts
File: src/container/services.ts
export type ApiServices = {
// ... existing services
newvendor: NewVendorPersonService;
};
const createServices = (...) => {
// ... existing service creation
const newvendor = input?.newvendor ?? DefaultNewVendorPersonService.getInstance(person);
const providerFactory = new ProviderFactory(
apollo,
lusha,
cognism,
rocketreach,
newvendor, // β Add new vendor
dataDescription,
cacheStorage
);
const services: ApiServices = {
// ... existing services
newvendor,
};
return services;
};
Step 6: Create GraphQL API Layer
Create the GraphQL types and resolver in src/graphql-api/newvendor/.
a) Create Types
File: src/graphql-api/newvendor/types/newvendor-person.ts
@ObjectType("NewVendorPerson")
export class TypeNewVendorPerson { ... }
b) Create Input Types
File: src/graphql-api/newvendor/input/newvendor-person-lookup.ts
@InputType("InputNewVendorPersonLookup")
export class InputNewVendorPersonLookup { ... }
c) Create Resolver
File: src/graphql-api/newvendor/resolvers/newvendor-resolver.ts
@Resolver()
export class NewVendorResolver {
@Query(() => TypeNewVendorPerson, { nullable: true })
async newVendorPersonLookup(...) { ... }
}
d) Register Resolver
File: src/graphql-api/schema-resolvers.ts
import { NewVendorResolver } from "./newvendor/resolvers/newvendor-resolver";
export default [
// ... existing resolvers
NewVendorResolver,
];
Step 7: Create Data Repository Layer
If the vendor data needs to be persisted, create repositories in src/data/repos/newvendor/.
File: src/data/repos/newvendor/db-newvendor-person-repository.ts
export default class DbNewVendorPersonRepository
extends DbRepository<NVPerson>
implements NewVendorPersonRepository { ... }
File: src/data/repos/newvendor/cache-newvendor-person-repository.ts
export default class CacheNewVendorPersonRepository
extends CacheDbRepository
implements NewVendorPersonRepository { ... }
Model API Summary
| Step | Layer | Files |
|---|---|---|
| 1 | Domain | src/domain/newvendor/entity/, repository/, service/ |
| 2 | Services | src/services/newvendor/api-*.ts, default-*.ts |
| 3 | Provider Adapter | src/services/provider-enrichment/newvendor-enrichment-adapter.ts |
| 4 | Factory | provider-types.ts, provider-factory.ts |
| 5 | Container | src/container/types.ts, services.ts |
| 6 | GraphQL | src/graphql-api/newvendor/, schema-resolvers.ts |
| 7 | Data | src/data/repos/newvendor/ (if persistence needed) |
4. Commercial (Waterfall Integration)
Repository: cred-api-commercial
Prerequisite
The vendor must be integrated into the Model API first, exposing a providerEnrichment endpoint that returns standardized field values.
Step 1: Add Data Source Abbreviation Enum
File: src/domain/custom/entity/types.ts
Add the vendor abbreviation to CustomFieldRecordDataSourceEnum:
export enum CustomFieldRecordDataSourceEnum {
// ... existing values
/** Enrichment Providers */
AP = "AP",
LUSHA = "LUSHA",
COG = "COG",
ROCKETREACH = "ROCKETREACH",
NV = "NV", // β Add here (must match Model API abbreviation)
}
Step 2: Add Feature Constant
File: src/domain/feature/entity/feature.ts
Add a feature ID constant:
export const FEATURE_WATERFALL_DATASOURCE_NV = 99; // Use next available ID
Step 3: Add Feature Seed Entry
File: src/data/seeds/003-feature.ts
Add the feature configuration:
{
id: 99, // Must match the constant from Step 2
name: "Waterfall datasource (NewVendor)",
description: "NewVendor as data source for waterfall processing",
transactionType: FeatureTransactionType.CREDIT,
amount: 1, // Credit cost per enrichment
parentId: 24, // FEATURE_CUSTOM_FIELD_WATERFALL
dataSourceAbbreviation: "NV", // Must match enum abbreviation
},
Step 4: Register in DataSource Enrichment Provider
File: src/domain/enrichment/provider/datasource-enrichment-provider.ts
Add the vendor abbreviation to the supported providers list:
const PROVIDER_ENRICHMENT_ABBREVIATIONS: CustomFieldRecordDataSourceEnum[] = [
CustomFieldRecordDataSourceEnum.AP,
CustomFieldRecordDataSourceEnum.LUSHA,
CustomFieldRecordDataSourceEnum.COG,
CustomFieldRecordDataSourceEnum.ROCKETREACH,
CustomFieldRecordDataSourceEnum.NV, // β Add here
];
Step 5: Add Validation
File: src/domain/custom/usecase/field/create-or-update-many-custom-field-data-source.ts
Add the new feature ID to the validation condition:
} else if (
featureId === FEATURE_WATERFALL_DATASOURCE_CRED ||
featureId === FEATURE_WATERFALL_DATASOURCE_AP ||
featureId === FEATURE_WATERFALL_DATASOURCE_LUSHA ||
featureId === FEATURE_WATERFALL_DATASOURCE_COG ||
featureId === FEATURE_WATERFALL_DATASOURCE_ROCKETREACH ||
featureId === FEATURE_WATERFALL_DATASOURCE_NV // β Add here
) {
await this.validateCREDDataDescription(input);
}
Step 6: Add Default Configurations (Optional)
File: src/data/seeds/005-custom-field-waterfall-config.ts
If the vendor should be a default source for certain fields in new workspaces:
{
templateName: "email",
entityType: EntityTypeEnum.CONTACT,
priority: 3,
featureId: FEATURE_WATERFALL_DATASOURCE_NV,
isEnabled: false, // Disabled by default
credDataDescriptionId: "xxx", // Get from DBT DataDescription
dataDescriptionEntityType: EntityTypeEnum.PERSON,
},
Commercial Summary
| Step | File | Change |
|---|---|---|
| 1 | types.ts |
Add to CustomFieldRecordDataSourceEnum |
| 2 | feature.ts |
Add FEATURE_WATERFALL_DATASOURCE_* constant |
| 3 | 003-feature.ts |
Add feature seed entry |
| 4 | datasource-enrichment-provider.ts |
Add to PROVIDER_ENRICHMENT_ABBREVIATIONS |
| 5 | create-or-update-many-custom-field-data-source.ts |
Add to validation |
| 6 | 005-custom-field-waterfall-config.ts |
Add default configs (optional) |
Cross-Project Verification
Before testing, verify naming consistency across all projects:
| Check | CRED Model (cred-model) |
DBT (cred-dbt) |
Model API (cred-model-api) |
Commercial (cred-api-commercial) |
|---|---|---|---|---|
| Abbreviation | Table: NVPerson |
dataSourceAbbreviation: "NV" |
ProviderDataSourceAbbreviation.NV |
CustomFieldRecordDataSourceEnum.NV |
| Table Name | NVPerson in migration |
tableName: "NVPerson" |
Entity: NVPerson |
N/A |
| Provider ID | Column: providerUniqueId or nvId |
N/A | Entity field: nvId |
N/A |
!!! danger "Common Mistakes" - Using different abbreviations across projects (e.g., NV in Model API but NEW_VENDOR in Commercial) - Mismatched table names between CRED Model migration and DBT config - Forgetting to add the sync config entry in CRED Model - Not adding the provider to ProviderFactory switch statement
Checklist
Use this checklist when adding a new vendor:
- CRED Model: Database migration and sync config created
- DBT: Provider table added to DataDescription
- Model API: Full integration (domain, services, adapter, factory, GraphQL)
- Commercial: Waterfall integration (enum, feature, seed, validation)
- Naming Consistency: Same abbreviation used in ALL projects
- Testing: End-to-end tested in development
- Documentation: Vendor added to relevant docs