Skip to main content
The data-plane keeps its own PostgreSQL database. It holds configuration and operational records only — which systems to read, how to shape what they produce and where to deliver it, plus who may change any of that. Replicated data is never written here: it travels source → broker → target, and no row of yours is copied into this database along the way. The one exception is quoted, not copied — a failure is stored as the system that raised it worded it, and such a message can name the row it refused. Every such column is named at the end of this document. 30 tables in 6 subject areas, 53 references between them.

How to read this

  • An arrow points from the row holding the reference to the row it names. subscription → connector means a subscription row stores the id of a connector row.
  • A dashed arrow is a reference with no foreign key behind it. Always deliberate, and always for the same reason: the referenced row may be deleted while the referring row has to survive it. Every one is listed, with its reason, at the end of this document.
  • PK is the primary key, FK a reference, UK a value unique across the table. Every table is keyed by a UUID v4 generated by the application, and carries created_at / updated_at.

Map

Access and organisation

Who may act, and on what. Environments hold teams, teams hold everything else, and membership in either is what every permission check reads.

env

The top of the hierarchy: one environment, its teams, and the broker they share. Environments are synchronised from the control-plane by UUID, which is why two data-planes must never share a database — the names would collide.

env_member

A user’s membership of an environment, and the role it grants there. One row per (user, env): a duplicate makes every later permission check raise, which is why the write path takes an advisory lock rather than relying on a read-then-insert. References: user_id → user.id, ON DELETE CASCADE; env_id → env.id, ON DELETE CASCADE. Indexed on: (user_id, env_id).

env_request

A pending join or invitation to an environment, and the role acceptance would grant. The same row shape covers both directions: a user asking to join and an admin inviting differ only in who from_user_id is. References: env_id → env.id, ON DELETE CASCADE; from_user_id → user.id; to_user_id → user.id. Indexed on: (env_id); (from_user_id); (to_user_id).

service_account_user

A named machine identity, backed by a real user row. Its token carries both ids, and every permission check reads the user — the service account only gives the token a name and a lifetime of its own. References: user_id → user.id, ON DELETE CASCADE.

team

The unit of ownership: every pipeline and connector belongs to exactly one. Team membership is what a write is authorized against, so a team is also the smallest thing two people can be given different access to. References: env_id → env.id, ON DELETE SET NULL. Unique together: (env_id, name).

team_member

A user’s membership of a team — the row every write permission check reads. One row per (user, team), for the same reason EnvMember is: a duplicate makes every later check raise. An API key’s memberships are derived from its grants and written here too, so authorization has one shape whoever the caller is. References: user_id → user.id, ON DELETE CASCADE; team_id → team.id, ON DELETE CASCADE. Indexed on: (user_id, team_id).

team_request

A pending join or invitation to a team, and whether acceptance would grant admin. The same row shape covers both directions: a user asking to join and an admin inviting differ only in who from_user_id is. References: team_id → team.id; from_user_id → user.id; to_user_id → user.id. Indexed on: (from_user_id); (team_id); (to_user_id).

user

A person or machine identity that can act on this data-plane. Also backs every service account: a service-account token carries both ids and every permission check reads the user, so there is no second identity table. References: active_env_id → env.id, ON DELETE SET NULL. Indexed on: unique (email); (updated_at); (version).

Pipelines

The configured data flow itself: a source connector reads a system, a datamodel shapes what it produced, and a subscription delivers that into a target connector.

connector

An integration point with one external system, read from or written to. Whether it is a source or a target is derived from its type, never chosen; its configuration is encrypted at rest, so anything selecting json_configuration — even through a join — owes a decrypt (#3698). It runs on one worker per shard of its plan. References: team_id → team.id; env_id → env.id. Unique together: (env_id, name). Indexed on: (lower(name)).

datamodel

The shape of what a source produced, and the topic it is republished on. Sits between the raw CDC topic and the subscriptions: one datamodel feeds as many subscriptions as there are places the data has to land. References: source_connector_id → connector.id, ON DELETE SET NULL; error_table_target_id → connector.id (no foreign key: Optional pointer at the connector receiving rejected rows; unset on most datamodels.). Indexed on: (lower(name)); (source_connector_id, source_table).

pipeline

One configured flow from a source connector to a target connector. A pipeline has no state column of its own: what the UI shows is its worker’s state, and “draft” is the computed absence of one — is_completed false with the worker paused. References: team_id → team.id; env_id → env.id; source_connector_id → connector.id; target_connector_id → connector.id; datamodel_id → datamodel.id. Unique together: (env_id, name). Indexed on: (lower(name)).

subscription

One delivery: how a datamodel’s rows are transformed and written into one target table. Identified by (datamodel, target connector, target table) where the index below applies; where it does not, that triple names a set rather than a row (#4443). References: pipeline_id → pipeline.id, ON DELETE SET NULL; datamodel_id → datamodel.id, ON DELETE SET NULL; target_connector_id → connector.id, ON DELETE SET NULL; error_table_target_id → connector.id (no foreign key: Optional pointer at the connector receiving rejected rows; unset on most subscriptions.). Indexed on: unique (datamodel_id, target_connector_id, target_table_name).

worker

The execution state of one of a connector’s pods — both what we want and what we observed. state carries the two at once: STOPPING is an intent, LIVE an observation, so every runtime write goes through Worker.accepts_reported_state() under a row lock. A heartbeat older than 90 s means the pod stopped reporting, not that it stopped. One row is one shard: a connector holds as many as its plan needs, dense from index 0 (#4146). The uniqueness that used to say “one pod per connector” now says “one pod per slice”, which is what keeps a re-plan from double-launching a shard. References: connector_id → connector.id, ON DELETE CASCADE. Unique together: (connector_id, shard_index).

Schema registry

The Avro schemas seen on the topics, de-duplicated by content hash and versioned per subject.

schema

One schema document, stored once per distinct content. De-duplicated by hash: the same Avro schema registered under twenty subjects is one row here and twenty SchemaVersion rows pointing at it.

schema_version

One version of one subject: what a topic’s key or value looked like at a point in time. The history is what lets a consumer read a message written before the current schema. References: schema_id → schema.id.

Throughput metrics

Append-only evidence of what actually moved: raw snapshots pushed by workers, and the closed-day rollups the charts read.

consumption_metric

Append-only table for per-subscription consumption metrics. Each row represents one consumption snapshot for a given subscription/topic over the interval [from_ts, to_ts] (epoch ms). Distinct from metric which stores source-side production counts: two subscriptions on the same source topic produce two independent rows here, capturing their actual delivery progress (consumed/delivered/skipped/errored/lag). References: subscription_id → subscription.id (no foreign key: Deleting a subscription must not erase what it already delivered, and no FK action means ‘keep the row and the id’.). Indexed on: (created_at); (subscription_id, created_at).

consumption_metric_daily

Pre-aggregated daily rollup of consumption_metric, one row per (subscription_id, day). Populated by the metric rollup background task for closed UTC days only, mirroring ProductionMetricDaily. Nothing reads it yet: the dashboard summary is production-only, and the per-subscription detail path (raw rows + live lag) must keep reading raw — this table is the substrate for the detail-page aggregation follow-up flagged in #3200. consumer_lag is deliberately absent: it is a point-in-time gauge, not a summable counter, and its live value comes from the broker at read time. No FK on subscription_id — evidence must survive subscription deletion. References: subscription_id → subscription.id (no foreign key: Same rule as the raw rows it aggregates: a rollup outlives the subscription.). Indexed on: (day); unique (subscription_id, day).

production_metric

Append-only table for CDC event production metrics pushed by source workers. Each row represents one metric snapshot for a given topic, covering the time interval [from_ts, to_ts] expressed as epoch timestamps (ms). created_at (inherited from Base) acts as the ingestion timestamp. Paired with ConsumptionMetric (delivery counts on the target side). References: topic → datamodel.source_topic (no foreign key: Production is counted per Kafka topic, and a topic outlives the datamodel that named it; the read path resolves the topic set, then filters on it.). Indexed on: (created_at); (topic, created_at).

production_metric_daily

Pre-aggregated daily rollup of production_metric, one row per (topic, day). Populated by the metric rollup background task for closed UTC days only; the monitoring read path unions this table with raw production_metric rows for the still-open day. Recomputed idempotently (delete + insert per day), so a crashed or duplicated rollup cycle is harmless. Like the raw metric tables, this is derived evidence keyed by a plain topic string — no FK, so history survives topic renames and datamodel deletions. span_ms / min_from / max_to / row_count preserve the message-rate inputs the summary math needs (_effective_metrics_duration_ms); without them msg/s would silently change versus raw aggregation. References: topic → datamodel.source_topic (no foreign key: Same rule as the raw rows it aggregates.). Indexed on: (day); unique (topic, day).

raw_metric_retention_state

One raw metric table’s last retention catch-up, last prune failure, and how long its index has been invalid.

Transformations

The compute engines the orchestrator runs transformations on. A warehouse runs no worker: statements are relayed to it and the query executes on the customer’s own compute.

task

A SQL script that runs on its warehouse on a schedule; each run is a warehouse_script_run row. Its state (draft, building, live, error, paused) is computed from enabled and its last run, never stored. References: team_id → team.id; env_id → env.id; warehouse_id → warehouse.id, ON DELETE RESTRICT; created_by → user.id, ON DELETE SET NULL. Unique together: (env_id, name). Indexed on: (enabled, next_run_at); (warehouse_id).

warehouse

A credentialed connection to a compute engine, which the orchestrator relays statements to. Unlike a connector it runs no worker: queries execute on the customer’s own compute and only the statement travels. Its configuration holds an access token and is encrypted at rest like a connector’s (#3698), so anything selecting json_configuration owes a decrypt. References: team_id → team.id; env_id → env.id. Unique together: (env_id, name). Indexed on: (lower(name)).

warehouse_script_run

One run of a Workspace script on a warehouse, with every statement’s outcome. Stored rather than held in memory because the API runs several workers: the poll and the cancel can land on a worker other than the one executing the run, and both read this row. References: warehouse_id → warehouse.id, ON DELETE CASCADE; user_id → user.id, ON DELETE SET NULL; task_id → task.id, ON DELETE CASCADE. Indexed on: (task_id, created_at); (warehouse_id).

Operations

Everything that records an operator action or a run rather than a configuration: snapshot syncs, history backfills, the audit log, replay protection and the license verdict.

backfill_item

One (source table → destination) pair’s place in a run’s queue, and its Job’s outcome. References: backfill_run_id → backfill_run.id, ON DELETE CASCADE; subscription_id → subscription.id (no foreign key: A finished item is evidence of what was loaded and has to outlive the subscription it loaded; the same trade as the metric tables.); target_connector_id → connector.id (no foreign key: Part of the item’s identity — what was loaded, and where — so it outlives the connector for the same reason.); pipeline_id → pipeline.id (no foreign key: The pipeline the pair belonged to when the run was triggered; unset for a subscription outside one, and kept as recorded if the pipeline goes.). Indexed on: (backfill_run_id, position); unique (backfill_run_id, subscription_id); (pipeline_id); (target_connector_id).

backfill_run

One source connector’s backfill run (an ordered queue of table → destination pairs). References: source_connector_id → connector.id, ON DELETE CASCADE; triggered_by_user_id → user.id, ON DELETE SET NULL. Indexed on: (source_connector_id); unique (source_connector_id).

idempotency_record

Cached response for an (endpoint, idempotency_key) pair. Unique together: (endpoint, key). Indexed on: (expires_at).

license_state

Single cached row holding the latest license verdict from the control-plane.

resource_event

One situation on one connector, datamodel or subscription, folded over its repeats. References: connector_id → connector.id (no foreign key: An event is evidence of what happened to a connector and has to outlive it; the names beside the ids are denormalised for the same reason.); datamodel_id → datamodel.id (no foreign key: Set when the event is about one data model rather than about the whole connector; kept as recorded once the data model is gone.); subscription_id → subscription.id (no foreign key: Set when the event is about one subscription; kept as recorded once it is gone.). Indexed on: (connector_id, ts DESC); (datamodel_id, ts DESC); unique (fingerprint); (subscription_id, ts DESC).

resource_event_occurrence

One occurrence of an event, kept append-only so the topic can carry every repeat. Narrow on purpose: the publisher joins it back to the folded row for everything the record displays, and what cannot be reconstructed afterwards is exactly what is stored here — when the occurrence happened, and what the count had reached at that moment. References: event_id → resource_event.id, ON DELETE CASCADE. Indexed on: (event_id).

user_log

Audit trail of the actions users took on this data-plane. Append-only, and it outlives its actors: both user references are SET NULL and the email is denormalised, so deleting a user redacts the link without erasing the action. References: user_id → user.id, ON DELETE SET NULL; target_user_id → user.id, ON DELETE SET NULL.

References that are not foreign keys

Each of these points at another table without the schema saying so, and each does it for the same reason: the row it names may be deleted while this row has to outlive it.

What is sensitive, and what is not