Skip to main content

Data model

Launchpad joins a Supabase configuration plane to a ClickHouse analytics plane. The stores use different identifiers deliberately: Supabase clients.id is the relational primary key, while ClickHouse client_id carries clients.slug as the analytics tenant key.

Client
└── Ad account / source
└── Campaign
└── Ad set
└── Ad
└── Creative

Campaign, ad-set, ad, and creative identifiers are separate. One creative asset may appear in more than one ad, so ad_id cannot be replaced by creative_id.

Canonical paid row​

PaidAcquisitionRow in src/lib/marketing/row.ts is the exact TypeScript contract for marketing_paid_acquisition. Its identity and dimensions include:

interface PaidAcquisitionRow {
event_id: string
client_id: string // clients.slug
brand_id: string
account_id: string
account_name: string
synced_at: string
date: string
platform: string // facebook, google, tiktok, ...
campaign_id: string
ad_set_id: string
ad_id: string
creative_id: string
segment_key: string
country_code: string
device_type: string
age_group: string
gender: string
// names, other dimensions, and metric columns omitted here
}

Read the live interface before adding a fetcher. There is no raw payload column, the account field is account_id, and the segment field is segment_key.

Event identity and latest state​

paidEventId() hashes exactly these 12 pipe-delimited parts:

client_id | date | platform | campaign_id | ad_set_id | ad_id |
creative_id | segment_key | country_code | device_type | age_group | gender

That identity matches the ClickHouse ORDER BY grain. marketing_paid_acquisition is a ReplacingMergeTree(synced_at); deduplication is defined by the ordered grain and version, not by event_id alone. Use the maintained latest-state views or FINAL where the query contract requires it.

brand_id, account_id, names, and selected attributes are functionally dependent values rather than identity parts. Changing an attribute must not fork the fact grain.

Analytics families​

FamilyGrain / responsibility
marketing_paid_acquisitionPaid-media metrics at day + entity + segment grain
marketing_salesNormalized transactions and lifecycle classification
marketing_crm_order_eventsEvent-grain CRM/order lineage
marketing_attribution_eventsEvent-grain direct-attribution evidence with latest-state views
Creative/check tablesCreative metadata, transcript/QC outputs, and review state
Sync/activity tablesConnector execution and freshness evidence

Do not collapse platform purchases, CRM orders, and attributed sales into one denominator. Each metric keeps its own source and grain.

Reporting catalog and breakdowns​

The current reporting catalog is composed in src/lib/marketing/table/columnOptions.ts and src/lib/marketing/table/mergedColumns.ts; PerformanceReport consumes the merged catalog. Breakdown definitions live with the marketing types and segment/query code. Avoid hard-coded column or breakdown counts in documentation because both are generated/composed and change frequently.

Missing, unavailable, null, and numeric zero are distinct states. A source that was not queried or cannot support a grain must not be rendered as zero.

Configuration and credentials​

Supabase owns clients, users/RBAC, brands, account bindings, report settings, and credential pointers. client_ad_accounts.credential_ref points to sealed connector credentials; no connector secret belongs inline on the account row. See Schemas and migrations and Credentials and secret stores.