Kvindo™ Cloud Docs
A PostgreSQL Cluster is a managed, horizontally-shardable PostgreSQL deployment built on Citus (distributed PostgreSQL) and Patroni (automated failover), coordinated by a dedicated etcd quorum the platform provisions and manages for you — it never appears in the spec. The cluster is one resource with an array of named shard groups in spec.shardGroups; exactly one of them, marked isCoordinator: true, is the coordinator and holds the cluster's distributed-table metadata.
• Shard groups, not shards — each entry in spec.shardGroups is a full Patroni group (one primary + spec.replicasPerShardGroup replicas), not a single row range; Citus shards individual distributed tables across whichever shard groups exist
• Exactly one shard group is the coordinator, marked explicitly — set isCoordinator: true on exactly one entry, on every create and every modify; it is never inferred from array order. It can never be removed or handed off to a different shard group, but — unlike every other shard group, whose name is its real identity — its own name can be changed freely, since the server resolves that one entry by the flag instead
• Shard groups can't be swapped in one modify — adding a new shard group and removing an old one in the same request is rejected (it's indistinguishable from renaming one you meant to keep); do it as two separate modifies when you genuinely want to replace one
• Scaling is one shard group per modify — the shard-group count may change by at most one in a single request, same discipline as Valkey's own shard scaling
• No root password in the spec — this resource has no user-facing superuser credential. Create a PostgreSqlUser (see below) to get an actual login; the internal bootstrap credential the platform generates for its own automation is never exposed through the API
• Grow the volume, never shrink it — raise spec.volumeSizeGiB on a live cluster and the platform extends the disk and its filesystem in place, online, without a restart or failover. Decreasing it is refused: the block device cannot be made smaller, and nothing in the stack can shrink a filesystem online. A resize still needs free space on the compute node (about twice the growth), so a node that is itself nearly full can refuse it — support can relax that check per cluster
• Point-in-time restore — spec.restoreConfiguration creates a brand-new cluster restored from another cluster's backups as of a specific UTC moment; immutable, create-time only, and the two clusters are otherwise independent. The source cluster's PostgreSqlDatabase and PostgreSqlUser resources are cloned onto the newly-restored target cluster. The target must have the same shard-group count as the source, a volume size greater than or equal to the source's, and the same PostgreSQL major version as the source
• Public access — private by default; set spec.createPublicIpv4 for a public IPv4 per node, and spec.tlsMode to require to force encrypted connections (mandatory once public access is on). Once it is on, every entry in status.primaryEndpoints/status.readEndpoints is paired: that node's public address first, its private one second
Every cluster is backed up by pgBackRest into its own S3 bucket, which the platform creates and manages together with the cluster — a full backup on a schedule (kept for spec.backupRetentionDays) plus continuous WAL archiving, which is what makes a restore to an arbitrary moment possible.
• The Backups tab lists what is actually in the backup repository — every backup there is, per shard group, read from the backup storage itself rather than from the last recorded status, so backups taken outside the schedule are listed too. Each row shows the backup's label, type, start and finish times (UTC) and its size in the repository; a backup still running is marked as such, and one taken with page-checksum errors is flagged (it is still restorable, but its data is suspect). It also reports backups that already exist, not just the newest one — which is what a point-in-time decision needs
• Restoring always builds a NEW cluster — Restore from backup opens the create form pre-filled from this cluster with spec.restoreConfiguration set to the backup you picked. Nothing is created until that form is submitted, and the cluster you restored from is never modified: Patroni honours a custom bootstrap only while the new cluster's data directory is empty, and the API accepts restoreConfiguration at creation only, rejecting any later change to it
• Restoring to a moment after the newest backup is expected — WAL is archived continuously, so a point in time later than the last full backup is reached by replaying the archive forward; that is usually the moment you actually want. The usable window runs from the oldest retained full backup to now — anything earlier has already been removed by retention. This is the same spec.restoreConfiguration.restoreTime the API takes directly, as a UTC timestamp
A restored cluster starts with an empty backup repository of its own (its first backup is taken by the same scheduled job); the backups it was restored from stay in the source cluster's storage. Each shard group has its own independent backup chain, so a restore recovers all of them to the same moment.
Citus distributes tables by a chosen distribution column, and that choice shapes what queries stay fast. This is not a drop-in replacement for a single-node database once more than one shard group is in play — plan the schema around it:
• Primary keys must include the distribution column — Citus cannot enforce global uniqueness on any other column
• Foreign keys between distributed tables must reference the same distribution column, or the reference has to go through a reference table (a small table copied to every shard group instead of sharded)
• Joins and transactions that span rows with different distribution-column values cross shard groups — still correct, but noticeably slower than a same-node query, since it involves a real network round trip between shard groups
• Pick the distribution column deliberately — most applications shard on a tenant/customer/organization id, so that a single tenant's data (and its queries) stay within one shard group
None of this applies to a single-shard-group deployment — it behaves like ordinary PostgreSQL with Citus installed but never invoked.
Citus is licensed under the GNU Affero General Public License v3 (AGPLv3). Kvindo Cloud runs the unmodified upstream extension; its complete corresponding source code is publicly available from the upstream project at github.com/citusdata/citus, satisfying the AGPL's network-use source-availability requirement.
Every node runs the same four listeners in front of its local PostgreSQL. The status endpoint fields below each go through a different one — pick the field for what you're actually trying to do, not just whichever one is closest to hand:
• Port 5432 — raw PostgreSQL, direct to that one node. Bypasses HAProxy and the connection pooler entirely — no write-routing, no read load-balancing, no TLS-terminated pooled connections. This is what shardGroups[].primaryEndpoint/replicaEndpoints return: a specific node, not the cluster
• Port 8432 — HAProxy, write, routed to whichever node is the current leader of that shard group. This is what status.connectionUri, status.coordinatorEndpoint, and every entry in status.primaryEndpoints actually use — connect to any of them and Citus's own metadata routes a write or a distributed query to the correct shard group
• Port 7432 — HAProxy, read, load-balanced across that shard group's replicas (the primary is only used as a fallback). This is what every entry in status.readEndpoints uses
• Port 6432 — Odyssey, a TLS-terminating connection pooler in front of 5432. This is the intended entry point for a public-facing or high-connection-count client, and it's reachable on every node once spec.createPublicIpv4/spec.tlsMode are set — but no status field reports it today. Until that's surfaced, derive it yourself: take the host from any endpoint above and connect to port 6432 on it directly
status.port is a fixed default (5432) that is never actually reassigned from any
live node — it does not describe the port any of the endpoint fields above use, including its
own neighbor status.connectionUri (8432). Read the host:port pairs in the endpoint
fields themselves rather than combining a host from one field with status.port.
| UI Name | API / kc name | Terraform name | Optional | Format | Default | Is read only | Description |
|---|---|---|---|---|---|---|---|
| Shard groups | spec.shardGroups |
spec.shard_groups |
No | list of objects | — | No | One entry per shard group — exactly one must have isCoordinator: true; that one can never be removed or handed off to a different shard group |
| Shard group name | shardGroups[].name |
shard_groups[].name |
No | string | — | No | For a non-coordinator entry, this is the real identity — the server resolves it against this cluster's existing shard groups by name and reuses the same underlying group when it matches; renaming is treated as removing the old one and adding a new one, so don't rename one you want to keep. The coordinator entry is the one exception: it's resolved by isCoordinator instead, so its name can be changed freely |
| VPC subnet | shardGroups[].vpcSubnetId |
shard_groups[].vpc_subnet_id |
No | ULID — references a VPC Subnet | — | No | Subnet for this shard group's instances (immutable after creation) |
| Is coordinator | shardGroups[].isCoordinator |
shard_groups[].is_coordinator |
No | boolean | false | No | Exactly one shard group per cluster must set this, on every create and every modify. Immutable once assigned — the coordinator can be renamed but never removed or replaced by a different shard group |
| Replicas per shard group | spec.replicasPerShardGroup |
spec.replicas_per_shard_group |
No | integer | — | No | Extra VMs per shard group replicating the primary for failover |
| Parameters set | spec.postgreSqlParametersSetId |
spec.postgre_sql_parameters_set_id |
Yes | ULID — references a PostgreSqlParametersSet | — | No | postgresql.conf tuning applied cluster-wide |
| Version | spec.version |
spec.version |
Yes | string — PostgreSQL major version | the only currently-accepted value | No | Only one (PostgreSQL, Citus) version pair is currently accepted — PostgreSQL 17.11 with Citus 14.2; no other value works. The version is immutable for the cluster's lifetime once set |
| Backup retention | spec.backupRetentionDays |
spec.backup_retention_days |
No | integer — days | — | No | How long pgBackRest archives are kept in S3 |
| Restore configuration | spec.restoreConfiguration |
spec.restore_configuration |
Yes | object | — | No | Point-in-time restore from another cluster's backups — immutable, create-time only. The source cluster's PostgreSqlDatabase and PostgreSqlUser resources are cloned onto the restored target |
| Source cluster | restoreConfiguration.postgreSqlId |
restore_configuration.postgre_sql_id |
No | ULID — references a PostgreSql cluster | — | No | The cluster whose backups to restore from. The target cluster must have the same shard-group count as this source cluster, a volume size greater than or equal to it, and the same PostgreSQL major version |
| Restore time | restoreConfiguration.restoreTime |
restore_configuration.restore_time |
No | string — UTC timestamp | — | No | Data after this moment is not present in the restored cluster |
| VM offer | spec.vmOfferId |
spec.vm_offer_id |
No | string (≤32) — compute offer | — | No | Applies to every instance across every shard group; see the Console catalog |
| Volume offer | spec.volumeOfferId |
spec.volume_offer_id |
No | string (≤32) — disk offer | — | No | Applies to every instance. Changing it on a live cluster re-prices the disk; the size may have to be raised to whatever the new tier's minimum is |
| Volume size | spec.volumeSizeGiB |
spec.volume_size_gib |
No | integer — GiB | — | No | Applies to every instance. Can be increased on a live cluster — the platform grows the disk and its filesystem under a running PostgreSQL, with no restart — but never decreased |
| TLS mode | spec.tlsMode |
spec.tls_mode |
Yes | string — allow/require |
allow |
No | require is mandatory once spec.createPublicIpv4 is set |
| Public IPv4 | spec.createPublicIpv4 |
spec.create_public_ipv4 |
Yes | boolean | false |
No | Give every node a public IPv4 (immutable after creation) |
| Etcd members | spec.etcdInstances |
spec.etcd_instances |
Yes | list of objects | — | No | Where the Patroni DCS (etcd) members go. Omit it to have the platform provision one member in a dedicated, isolated etcd VPC; supply exactly 1 or 3 {id, vpcSubnetId} entries to place them yourself, which replaces that network entirely. An empty list is a request for zero members and is rejected. The count and the id set are immutable after creation, and there is no way back to self-provisioning — recreating the cluster is the only path |
| Etcd member id | etcdInstances[].id |
etcd_instances[].id |
No | ULID | — | No | Becomes that member's Vm and Volume id in the underlying transaction, so it must be stable across every modify — re-send the committed id, never a fresh one |
| Etcd member subnet | etcdInstances[].vpcSubnetId |
etcd_instances[].vpc_subnet_id |
No | ULID — references a VPC Subnet | — | No | Immutable after creation: moving a live DCS member to another subnet is rejected |
| Etcd public IPv4 | spec.etcdCreatePublicIpv4 |
spec.etcd_create_public_ipv4 |
Yes | boolean | true (public) |
No | Whether each DCS member gets a floating IP. Only valid together with spec.etcdInstances — a self-provisioned member lives in a VPC that is never peered, so a private one there would be unreachable. Setting it to false requires that the members' VPC has a NAT floating IP (a private member has no egress of its own, and the etcd install needs apt and GitHub) and that it is peered with every shard group's VPC. Immutable after creation — delete and recreate to change it. TLS and etcd RBAC stay mandatory either way |
| Port | status.port |
status.port |
— | integer | — | Yes | A fixed default (5432), never reassigned from any live node — it does not describe the port any endpoint field on this page actually uses. See Connecting: ports and pooling above |
| Connection URI | status.connectionUri |
status.connection_uri |
— | string | — | Yes | A starting point — a real client still needs to route reads/writes appropriately. Uses HAProxy's write port (8432), same as coordinatorEndpoint/primaryEndpoints, and the leader's PUBLIC address on a cluster created with createPublicIpv4 |
| Coordinator endpoint | status.coordinatorEndpoint |
status.coordinator_endpoint |
— | string | — | Yes | host:port (HAProxy write, 8432) of the coordinator shard group's current primary. PUBLIC address first on a cluster created with createPublicIpv4 |
| Primary endpoints | status.primaryEndpoints |
status.primary_endpoints |
— | list of strings | — | Yes | Every node's write entry point (HAProxy write, 8432) — the coordinator, every other shard group's primary, and every replica, not just the shard-group leaders. Connect to any entry: each node runs its own HAProxy and every node's Citus metadata is kept in sync, so any of them routes a write to the correct shard group's current leader and can plan/execute a distributed query across the whole cluster. On a cluster created with createPublicIpv4 this is TWO entries per node — that node's public address first, then its private one. The first entry of each pair is the one reachable from outside the VPC, so primaryEndpoints[0] is the entry point most callers want; a client that round-robins the whole list is choosing between two networks and must filter |
| Read endpoints | status.readEndpoints |
status.read_endpoints |
— | list of strings | — | Yes | Every node's read entry point (HAProxy read, 7432, load-balanced across that shard group's replicas). Same public-first-then-private pairing, and the same warning about round-robining the list, as primaryEndpoints above. Offloads only plain (non-Citus) tables to a replica — distributed and reference tables are still served by the shard's primary regardless of which node you connect to, because Patroni standbys aren't registered as Citus secondary nodes yet. Check citusManagedTableCounts to see how much of your schema is actually being offloaded |
| Citus managed table counts | status.citusManagedTableCounts |
status.citus_managed_table_counts |
— | map of string to integer | — | Yes | How many tables of each Citus type exist — keyed by type (distributed, reference, ...), not by individual table name. This is the read endpoint's own honesty check: a non-empty value here means some of your tables are NOT being offloaded to a replica |
| Cluster state | status.clusterState |
status.cluster_state |
— | string | — | Yes | Reported directly from the underlying probe |
| Anti-affinity OK | status.antiAffinityOk |
status.anti_affinity_ok |
— | boolean | — | Yes | False when a primary and one of its own replicas share a VM (degraded placement) |
| Anti-affinity message | status.antiAffinityMessage |
status.anti_affinity_message |
— | string | — | Yes | Human-readable detail behind antiAffinityOk |
| Shard groups | status.shardGroups |
status.shard_groups |
— | list of objects | — | Yes | Observed state per shard group |
| Is coordinator | shardGroups[].isCoordinator |
shard_groups[].is_coordinator |
— | boolean | — | Yes | The observed truth from the live cluster, not an echo of the spec field of the same name — reports whether this shard group is genuinely running as Citus's coordinator group right now |
| Primary endpoint | shardGroups[].primaryEndpoint |
shard_groups[].primary_endpoint |
— | string | — | Yes | host:port of this shard group's current primary — raw PostgreSQL (5432), a specific node, bypassing HAProxy and the connection pooler |
| Replica endpoints | shardGroups[].replicaEndpoints |
shard_groups[].replica_endpoints |
— | list of strings | — | Yes | host:port for each of this shard group's replicas — raw PostgreSQL (5432), same bypass as primaryEndpoint above |
| Nodes | status.nodes |
status.nodes |
— | list of objects | — | Yes | One entry per provisioned instance across all shard groups |
| Public IPv4 | nodes[].publicIpV4 |
nodes[].public_ipv4 |
— | string, may be null | — | Yes | The node's own floating IP — present only on a cluster created with createPublicIpv4, and null on every etcd node. This is the address the public entries in primaryEndpoints/readEndpoints are built from |
| Private IPv4 | nodes[].privateIpV4 |
nodes[].private_ipv4 |
— | string | — | Yes | Address on the shard group's VPC subnet |
| Is primary | nodes[].isPrimary |
nodes[].is_primary |
— | boolean | — | Yes | Whether this instance is currently its shard group's primary |
| Patroni state | nodes[].patroniState |
nodes[].patroni_state |
— | string | — | Yes | Raw Patroni member state |
A private 2-shard-group cluster (real Citus sharding) with one replica per shard group, on an existing subnet.
Terraform
Python SDK
kc CLI
Resource docs
Examples