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. Your rows never land here: replicated data travels source → broker → target and is never written to this database. 24 tables in 5 subject areas, 37 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_iduser.id, ON DELETE CASCADE; env_idenv.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_idenv.id, ON DELETE CASCADE; from_user_iduser.id; to_user_iduser.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_iduser.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_idenv.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_iduser.id, ON DELETE CASCADE; team_idteam.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_idteam.id; from_user_iduser.id; to_user_iduser.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_idenv.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). Exactly one worker runs it. References: team_idteam.id; env_idenv.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_idconnector.id, ON DELETE SET NULL; error_table_target_idconnector.id (no foreign key: Optional pointer at the connector receiving rejected rows; unset on most datamodels.). Indexed on: (lower(name)).

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_idteam.id; env_idenv.id; source_connector_idconnector.id; target_connector_idconnector.id; datamodel_iddatamodel.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), because fanning one topic out into several tables of the same target is a shape the wizard produces on purpose. References: pipeline_idpipeline.id, ON DELETE SET NULL; datamodel_iddatamodel.id, ON DELETE SET NULL; target_connector_idconnector.id, ON DELETE SET NULL; error_table_target_idconnector.id (no foreign key: Optional pointer at the connector receiving rejected rows; unset on most subscriptions.). Unique together: (datamodel_id, target_connector_id, target_table_name).

worker

The execution state of one connector’s pod — 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. References: connector_idconnector.id, ON DELETE CASCADE.

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_idschema.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/errored/lag). References: subscription_idsubscription.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: (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 (see docs/claude/architecture-decisions.md). References: subscription_idsubscription.id (no foreign key: Same rule as the raw rows it aggregates: a rollup outlives the subscription.). Indexed on: 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: topicdatamodel.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: (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 (see docs/claude/architecture-decisions.md). 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: topicdatamodel.source_topic (no foreign key: Same rule as the raw rows it aggregates.). Indexed on: unique (topic, day).

Operations

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

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.

sync_run

A connector’s current sync (blocking snapshot of 1..N tables, driven one at a time). References: connector_idconnector.id, ON DELETE CASCADE; triggered_by_user_iduser.id, ON DELETE SET NULL. Indexed on: unique ((CASE WHEN status = 'SYNCING' THEN connector_id END)); (connector_id).

sync_table

One table’s place in a sync queue plus its blocking-snapshot progress. References: sync_run_idsync_run.id, ON DELETE CASCADE. Indexed on: (sync_run_id, group_id); (sync_run_id, position).

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_iduser.id, ON DELETE SET NULL; target_user_iduser.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