Chapter 60 · Setting Up A Data Warehouse Source
Subchapter 60.1
references/sync-types.mdMarkdown7 KBView on GitHub
Every table in a data warehouse source needs a sync_type. The choice determines how data flows on every sync, how
much it costs, how fresh the data is, and what shape it has after import.
| Sync type | What it does | Requires | Good for |
|---|---|---|---|
full_refresh | Drops everything, re-imports the whole table each sync | Nothing | Small tables, dimension tables, when in doubt |
incremental | Imports only rows where incremental_field > last_value | incremental_field + incremental_field_type | Large tables with an updated_at / modified_at |
append | Like incremental but keeps prior versions of rows | incremental_field + incremental_field_type | Immutable or append-only tables (events, logs) |
cdc | Streams changes via Postgres logical replication | Postgres, primary keys, wal_level=logical | Near-real-time, large Postgres tables |
webhook | Real-time push ingestion via source-side webhook | Source must implement WebhookSource | Near-real-time from sources that support it (Stripe) |
Not every source supports every type — the db-schema response surfaces which are available per-table via
incremental_available, append_available, cdc_available, supports_webhooks.
Small table, no obvious ordering column → full_refresh.
Anything under ~50k rows is cheap enough to full-refresh daily. If you can’t find a clean updated_at, don’t try to
fake incremental sync.
Large table with updated_at or modified_at → incremental with that field.
This is the common case. Each sync pulls only changed rows since the high-water mark. Much cheaper.
Large table but only created_at → incremental with created_at, but warn the user.
created_at only catches new rows. Updates to existing rows will be missed. Fine for immutable records; wrong for
mutable ones.
Large table with no good timestamp but a monotonic integer id → incremental with the id (type integer).
Works the same way — new rows have higher ids.
Immutable events / logs table → append if available, else incremental.
Append preserves every version. Useful when the source updates records and you want to track the history rather than
latest-state only.
Postgres, you need sub-minute freshness → cdc.
CDC streams WAL changes instead of polling. Requires primary keys on each table and Postgres logical replication
setup. Run external-data-sources-check-cdc-prerequisites-create first — unmet prerequisites cause the sync to fail
immediately on first run.
Stripe (or any source where supports_webhooks: true) → consider webhook for real-time push.
Set sync_type: "webhook" on the tables that support it. Note that a webhook-type schema still does an initial
bulk load before switching to push mode, and keeps a sync_frequency for periodic reconciliation. You also need to
call external-data-sources-create-webhook-create after source creation — setting the sync_type is necessary but
not sufficient. Tables on the same source that don’t support webhooks need a normal bulk sync_type alongside.
Must be present in the incremental_fields list returned by db-schema. Not every column qualifies — the source
only surfaces timestamp/integer/ObjectID columns that it can filter cheaply.
Priority order:
updated_at / modified_at / last_modified / hs_lastmodifieddate — catches both inserts and updates.created_at / inserted_at — catches inserts only, not updates. Only use when you know the table is immutable.id, sequence_number) — same tradeoff as created_at._id (ObjectId) — sortable by creation time, natural fit for Mongo.Must match the chosen field:
datetime — TIMESTAMP, DATETIME, TIMESTAMP WITH TIME ZONEdate — DATEtimestamp — unix-epoch integer timestampsinteger — INT, BIGINT, SERIALnumeric — NUMERIC, DECIMAL where the values are monotonically increasingobjectid — MongoDB ObjectIdIf the chosen field and the declared type don’t match, the first sync will fail with a type coercion error.
Required for cdc. Strongly recommended for incremental on tables that can have updates, because it’s used for
upsert deduplication.
detected_primary_keys from db-schema when available.cdc — it will fail with “Primary key required for incremental
syncs” or “primary keys for this table are not unique”.For CDC schemas, cdc_table_mode decides what the synced table looks like:
consolidated (default) — the table reflects the current state of each row. Old versions are overwritten.
Best for operational queries.cdc_only — the table is an append-only log of every change event (insert / update / delete). No current-state
view.both — keep a consolidated current-state table and a separate CDC log table. Most flexible, doubles storage.Unlike the other sync types, webhook needs a second API call after source creation:
sync_type: "webhook" on webhook-eligible tables.external-data-sources-create-webhook-create({id}) to register the webhook with the external service and create
the HogFunction that processes incoming events.If step 2 is skipped, webhook-type schemas sit there with no data flowing. If the external service doesn’t allow
PostHog to auto-register (usually because the stored API key lacks webhook permissions), fall back to manual setup:
register the webhook in the source’s dashboard, then submit the signing secret via
external-data-sources-update-webhook-inputs-create.
Only source types that implement WebhookSource in the codebase support sync_type: "webhook". Today that’s Stripe.
The supports_webhooks flag on each table in the db-schema response is the source of truth for what’s available.
sync_frequency is a per-schema option that’s orthogonal to sync_type. Valid values are (smallest to largest):
"1min", "5min", "15min", "30min", "1hour", "6hour", "12hour", "24hour", "7day", "30day", and
"never".
full_refresh on anything non-trivial: default to 24hour — it re-imports everything each run. Sub-hour
frequencies are usually wasteful for full refresh.incremental / append: 1hour or 6hour is a reasonable default on reasonably sized tables. Sub-minute
frequencies (1min, 5min) exist but are rarely needed — if the user wants real-time, prefer cdc or webhook.cdc: frequency doesn’t really apply — CDC streams continuously.7day or 30day keeps the schedule alive without wasting runs.The never value freezes the schema — it won’t sync automatically, but can still be triggered manually via
external-data-schemas-reload.