Skip to main content
Every Popsink installation is two halves. The data plane is the runtime that moves your data; the control plane is the fleet manager that knows your deployments, users and entitlements. In SaaS both are operated by Popsink. In self-hosted, BYOC and on-prem, you run the data plane and it attaches to a control plane — ours at control-plane.popsink.com, or your own. The interesting question for a security review is not the diagram, it is the wire: what actually crosses between them. This page answers it field by field.

Who owns what

Control plane

Organizations, users, environment memberships, deployment registry, licensing, usage and billing. It is the source of truth for identity — accounts and roles are defined here, never in the data plane.

Data plane

Pipelines, connectors, datamodels, subscriptions, the broker, the schema registry and every connector worker pod. It is the only place your records exist — the control plane never sees them.

Direction of travel

The data plane always initiates. The control plane never calls in.
There is no webhook, callback or polling API that the control plane invokes on a data plane. Everything the control plane wants to say travels back as the response to a call the data plane made. This is what makes the network requirement one-directional: The only apparent exception is the browser: the control plane issues HTTP 302 redirects that send the user’s browser to your ingressUrl (login return, connector OAuth callbacks). That is a redirect the browser follows, not a connection the control plane opens.

The channels

Five distinct exchanges, all authenticated with the deployment JWT: Only one data-plane replica performs the periodic ones — a PostgreSQL advisory lock elects the sender, so scaling replicaCount does not multiply heartbeats.

Authentication

The deploymentJwtToken from the onboarding wizard is sent verbatim as Authorization: Bearer <jwt> on every call above. It is an HS256 token whose claims are just the deployment’s identity:
The deployment JWT has no exp claim and is never rotated automatically — it represents long-lived infrastructure. Rotating it means updating the Kubernetes secret and restarting the data-plane pods. Treat it as a credential of the same weight as a database password.
The control plane resolves the deployment from the token and rejects the request with 403 if the deploymentId in the path does not match — a stolen token cannot be pointed at someone else’s deployment.

The heartbeat

The heartbeat is the main channel. It is a status report and the trigger for config reconciliation, in a single round trip every ten minutes.

What the data plane sends

worker_images reports which connector images your workers actually run, so the control plane can tell you a worker is behind the chart:
If the data plane cannot inventory its workers, the field is omitted rather than sent empty. This holds for worker_images, data_plane_version and supported_env_roles alike: an omitting heartbeat never clears what the control plane already stored, so a transient read failure cannot look like a regression.
supported_env_roles is a capability declaration. A deployment that has never reported it is assumed to understand only admin and user — which is why assigning the reader role to someone on an older data plane silently grants write access instead. Upgrade the data plane before relying on reader.
entity_snapshot is a flat inventory of your pipelines, stored against the deployment and used by the Snowflake marketplace integration. Per object it carries identifiers, the user-chosen name and a state — and nothing else: Note what is absent: no connector configuration, no host, no credentials, no schema, no column list, no transform code.

What the control plane answers

Licensing

The control plane computes the verdict; the data plane enforces it. The control plane never blocks anything itself. The verdict is cached in the data-plane database so every replica shares it, and it defaults to false — an older or misbehaving control plane can never imply “licensed”. Enforcement is deliberately forgiving: When it blocks, the data plane answers 403 with {"code": "license_inactive"}. The grace period is LICENSE_GRACE_PERIOD_SECONDS, and enforcement ships dark unless LICENSE_ENFORCEMENT_ENABLED is on.
The practical reading for an operator: a control plane you cannot reach does not take your pipelines down. Heartbeats fail, the deployment shows offline, config stops reconciling and new SSO logins fail — but data keeps moving, and the cached license verdict holds for a week.

Deployment status

The control plane derives your deployment’s status entirely from heartbeat arrival. There is no probe. A self-hosted deployment starts offline: the control plane provisions nothing and can only wait for you to run helm install. That is why the self-hosted wizard sits on Awaiting connections… until the first heartbeat lands, then flips the deployment to Live.
Heartbeats go out every 10 minutes. The control plane sweeps every 5 minutes and marks a deployment offline when the last heartbeat is older than 13 minutes — so a single missed heartbeat is tolerated, two are not.
The practical consequence for upgrades: after helm upgrade, allow up to a minute for the status to return to Live, and expect a brief offline window if a rollout takes longer than 13 minutes. Because the heartbeat reports data_plane_version, the control plane can compare what you run against the chart version it publishes as supported — which is why the install wizard never hardcodes a chart version and you should not pin one either. See Upgrading a fleet.

Config sync

Environments, members and users flow control plane → data plane, never the other way. The control plane owns them; the data plane holds a replica. Reconciliation is hash-based rather than push-based:
1

Both sides hash the same structure

The data plane rebuilds its local environment/member config and hashes it. The control plane hashes its own. The two builders are deliberately identical — same field names, same role vocabulary — so the hashes converge.
2

The heartbeat compares them

The data plane sends its config_hash; the response carries the control plane’s and config_sync_required.
3

On drift, the data plane pulls

GET /deployment/{deploymentId}/config returns the full environment list with members and nested user records. The data plane applies it and deletes environments the control plane no longer lists.
4

Persistent drift backs off

If the same hash pair repeats, the re-sync is skipped for a few ticks rather than looping every cycle. Any change to either hash resumes it immediately.
The config payload contains member email addresses. Both planes explicitly refuse to log it in full; if you ship data-plane logs to a SIEM, this is the one control-plane payload that carries PII, and it is never written out.

Usage metrics

Every 30 seconds the data plane pushes aggregated CDC counters, which is what the control plane’s Usage tab and billing are computed from:
from_ts / to_ts are epoch milliseconds, and events are counts per operation code — read (snapshot), create, update, delete — emitted sparsely, so a key absent means zero. Row contents are not part of the payload, and there is no field that could carry one. The id is generated by the data plane and reused as the primary key on ingest, so a retried batch is idempotent rather than double-counted. This is the sole input to the control plane’s Usage tab and to billing; there is no second metering channel.
There is no infrastructure telemetry in this channel: no CPU, memory or disk metrics, no log shipping, no stack traces, no per-request tracing. If you want OpenTelemetry, it is a separate opt-in export to a collector you configure via OTEL_EXPORTER_OTLP_ENDPOINT — it does not go to the control plane.

Identity and login

In self-hosted mode the control plane is the identity provider. This is a design choice worth understanding before an install, because it means user login depends on control-plane reachability. These three calls are the only ones that carry a user email outbound (navigation, generate-switch-token, PATCH /deployment-users). Emails flow predominantly inbound, because the control plane is where accounts live. Do not confuse the two tokens involved: The user access token is what the control plane hands the browser after SSO; the data plane verifies it and exchanges it for its own session. It carries the user’s id, email, target deployment_id and whether they are an org admin.
If your data plane is reachable only on an IP literal or a kubectl port-forward address, the SSO redirect cannot come back to it. Use the paste-token page at /auth/token-login, and keep the chart’s adminCredentials account as the break-glass local login.

Connector OAuth brokering

For OAuth-based connectors (HubSpot, Google Ads, Salesforce…), the control plane holds the provider client_secret and the registered redirect_uri. Your data plane never registers an app with the provider and never holds the provider secret. The cost of that arrangement is explicit: the connector’s OAuth refresh token transits the control plane on every refresh, because the control plane is the only party that can exchange it. The refresh token originated at the control plane in the first place — it is handed to the data plane at grant time, and posted back to /oauth/{provider}/refresh to obtain a short-lived access token.

What never leaves the data plane

Verified against every outbound payload builder: Two caveats we would rather state than have you discover in an audit:
1. Kafka topic names embed the source schema.table. Topic names are {hashed-prefix}.{schema}.{table} — the prefix is a SHA-1 digest of the deployment/environment/connector IDs, but the trailing segment is plaintext. Topic names appear in both entity_snapshot and the usage metrics, so your table and schema names are visible to the control plane.2. User-chosen names are visible too — connector, datamodel, subscription, team and environment names, since they are what the control-plane UI displays.
If either is unacceptable for your data classification, name your tables, schemas and pipelines accordingly, or run in standalone mode.

Standalone: no control plane at all

Every exchange on this page is gated on deploymentMode: SELF_HOSTED. With deploymentMode: STANDALONE, the data plane makes zero calls to any control plane — the heartbeat and metrics tasks return immediately at startup, and no deploymentJwtToken is required. This is the mode behind air-gapped installations. The trade-offs are real and worth naming:

Air-gapped deployments

How offline, signed update bundles work.

Network requirements checklist

If the deployment stays offline, the cause is almost always one of the first two rows plus a wrong deploymentId / deploymentJwtToken — the full symptom-to-cause table is on the partners page.

Further reading

Deployments and environments

How to map deployments onto your regions and networks.

Install a deployment

Create the deployment in the control plane, then install the chart.

Deployment options

SaaS, BYOC, on-prem and air-gapped compared.

Security & compliance

Encryption, audit logging, certifications.