Skip to main content
Version: 0.4.x

XPlatformDatabaseCluster

Platform-managed CloudNativePG cluster with PgBouncer pooling, Vault credential seeding, Prometheus monitoring, Barman backups, and shared-pool labeling for dynamic tenant assignment.

Groupplatform.wxops.cloud
KindXPlatformDatabaseCluster
Pluralxplatformdatabaseclusters
ScopeCluster
API versionv1alpha1 (served, storage)
Composition functionfunction-kcl
This is a platform resource

XPlatformDatabaseCluster is managed by the platform team, not tenant developers. Tenants create databases through XTenantDatabase, which auto-assigns to a shared pool cluster or composes a child XPlatformDatabaseCluster as a dedicated resource. Platform engineers provision and tune the clusters; tenants only see the Vault-backed connection credentials.

PostgreSQL as a Multi-Modal Database Platform

The ambition behind XPlatformDatabaseCluster is more than hosting a standard relational database. PostgreSQL has a mature extension ecosystem that transforms a single database engine into a specialist system for different workload types — without running separate infrastructure for each.

A single XPlatformDatabaseCluster definition, combined with the right extension image and postgresConfig, becomes a purpose-built database for any of these profiles:

Extension profiles

ProfileExtensionImageappFlavor hintWhat it enables
Vector searchpgvectorpgvector/pgvector:pg16ai, ai-webappEmbedding similarity (<=>, <#>, <->), HNSW + IVFFlat indexes, RAG pipelines, semantic search
Time-seriestimescaledbtimescale/timescaledb:2.x-pg16Automatic time partitioning, continuous aggregates, compression, time_bucket() queries
Graphage (Apache AGE)custom age-pg16openCypher queries (SELECT * FROM ag_catalog.cypher(...)), graph traversals, property graphs
Geospatialpostgispostgis/postgis:16-3.4geo-webappGeometry types, spatial indexes (GIST), geographic functions, ST_Distance, ST_Within, map tiles
Full-text / hybrid searchpg_search (ParadeDB)paradedb/paradedb:pg16search-webappBM25 ranking, hybrid BM25+vector, @@@ operator, columnar analytics
Standard OLTPbuilt-inghcr.io/cloudnative-pg/postgresql:16webappJSONB, full SQL, pg_trgm, uuid-ossp, partitioning, row-level security

Why PostgreSQL extensions over separate systems

Running a dedicated vector database, a separate time-series system, and a standalone graph engine multiplies operational complexity — three different backup models, three credential systems, three sets of observability exporters. Each new system requires the platform team to build and maintain integration.

PostgreSQL extensions ship as a single CNPG cluster. The platform team builds or pulls an extension image once; XPlatformDatabaseCluster wraps it with the same pooling, backup, monitoring, and Vault credential seeding used for every other cluster. Tenants enable extensions per-database via XTenantDatabase.spec.parameters.extensions[].

Enabling extensions

Extensions are activated at two levels:

  1. Cluster image — the PostgreSQL image must have the extension shared library compiled in. Standard CNPG community images (ghcr.io/cloudnative-pg/postgresql:16) do not include extension libraries. Build or pull a custom image with the extension pre-installed.

  2. Database levelXTenantDatabase enables extensions inside a specific database via spec.parameters.extensions[]. The extension library must already be in the cluster image or installed separately.

The appFlavor label on XTenantApp (ai, geo-webapp, search-webapp) is the signal the platform team or scaffold wizard reads to select the right XPlatformDatabaseCluster and extensions when provisioning a tenant database.

Custom image for extension clusters

CNPG supports specifying a custom image per cluster. When provisioning a vector or PostGIS cluster, the platform team uses a pre-built image and points the cluster at it. A separate XPlatformDatabaseCluster per extension profile keeps standard and specialist workloads isolated — shared pools mix tenants, not extension types.

spec.parameters

Vault path convention

All PushSecret/ExternalSecret paths in this package omit the platform/ prefix (e.g. database-clusters/..., not platform/database-clusters/...). The vault-platform ClusterSecretStore is itself scoped to the platform/ KV2 path — adding platform/ again would write to platform/platform/....

Core

FieldTypeRequiredDefaultDescription
clusterNamestringyesCNPG cluster name. Tenants pass this as clusterRef in XTenantDatabase to target this cluster explicitly.
namespacestringyesKubernetes namespace where the CNPG cluster is created.
instancesinteger (1–9)1PostgreSQL instances. 1 = standalone (dev/test), 3 = HA with automatic failover.
postgresVersioninteger16PostgreSQL major version. Maps to ghcr.io/cloudnative-pg/postgresql:{version} for standard clusters; override the image at the CNPG level for extension profiles.
storageSizestring"8Gi"PVC storage per instance. Size according to expected data volume, not just initial data.
sharedbooleanfalseMark as available for the shared multi-tenant pool. When true, XTenantDatabase with tier: shared can auto-assign to this cluster. The XR manifest must also have wxops.cloud/managed-by: platform-database-clusters label.
environmentstring"dev"One of dev, staging, prod. When shared: true, XTenantDatabase only assigns databases whose environment matches — dev databases land on dev clusters, prod on prod.
vaultPlatformSecretStorestring"vault-platform"ESO ClusterSecretStore used to mirror CNPG's superuser and app secrets to Vault at database-clusters/{clusterName}/superuser-creds and database-clusters/{clusterName}/app-creds. When enablePooler is true, pooler connection strings are merged into both entries.

Pooler (RW)

FieldTypeDefaultDescription
enablePoolerbooleantrueDeploy a PgBouncer Pooler (type rw) alongside the cluster. Connection strings with pooler endpoints are included in the Vault entries.
poolModestring"transaction"PgBouncer pool mode: transaction (recommended for stateless apps) or session (required for apps that use advisory locks or SET LOCAL).

readReplica

Dedicated read-only pooler for replica traffic. Useful for reporting or analytics queries that should not compete with OLTP write traffic.

FieldTypeDefaultDescription
readReplica.enablePoolerbooleanfalseDeploy a second PgBouncer Pooler of type ro. Exposes {clusterName}-ro-pooler Service.
readReplica.poolerInstancesinteger (1–9)1

postgresConfig

PostgreSQL server configuration (postgresql.conf) and client auth (pg_hba.conf). Some parameters are owned by CNPG and cannot be overridden (archive_command, listen_addresses, wal_level, port, data_directory).

postgresConfig.tuning

Common sizing/tuning knobs. These are translated into postgresql.conf parameters.

FieldTypeDefaultDescription
tuning.maxConnectionsinteger200Max client connections. With PgBouncer, keep this low (100–300); without pooler, raise it.
tuning.sharedBuffersstringe.g. "256MB", "1GB". Rule of thumb: 25% of RAM.
tuning.workMemstringe.g. "16MB". Per sort/hash operation. High concurrency × high workMem = OOM risk.
tuning.maintenanceWorkMemstringe.g. "256MB". Used by VACUUM, CREATE INDEX, ALTER TABLE.
tuning.effectiveCacheSizestringe.g. "3GB". Hint to the planner: estimate of OS + Postgres caches combined.
tuning.maxWalSizestringe.g. "2GB". Max WAL size before a checkpoint is forced.
tuning.minWalSizestringe.g. "512MB".
tuning.checkpointCompletionTargetstringFloat as string, e.g. "0.9". Spreads checkpoint writes over 90% of the checkpoint interval to reduce I/O spikes.
tuning.randomPageCoststringFloat as string. Use "1.1" for SSD-backed PVCs (default planner assumption is "4" for spinning disks).
tuning.logMinDurationStatementstringMilliseconds, or "-1" to disable. e.g. "1000" logs queries slower than 1s.
tuning.logStatementstringOne of none, ddl, mod, all.
tuning.statementTimeoutstringe.g. "30s", "5min". Hard query kill after this duration.
tuning.idleInTransactionSessionTimeoutstringe.g. "10min". Kills sessions stuck in idle-in-transaction.

postgresConfig.extraParameters

Free-form postgresql.conf key-value pairs for parameters not covered by tuning. Keys here override the same key in tuning.

postgresConfig:
extraParameters:
track_io_timing: "on" # required for pg_stat_io visibility
wal_compression: "zstd" # reduces WAL volume ~3-5x
huge_pages: "try" # use huge pages if available
max_parallel_workers_per_gather: "4" # vector index scans benefit from parallelism

postgresConfig.pgHba

Additional pg_hba.conf rules, appended after CNPG's own required rules.

postgresConfig:
pgHba:
- "hostssl app all 10.0.0.0/8 scram-sha-256"

monitoring

FieldTypeDefaultDescription
monitoring.enablePodMonitorbooleanfalseCNPG creates a PodMonitor so Prometheus Operator scrapes the cluster metrics exporter on port 9187. Requires Prometheus Operator CRDs in-cluster.

backup

Barman object-store WAL archiving and scheduled base backups.

FieldTypeDefaultDescription
backup.enabledbooleanfalseActivates WAL archiving and creates a ScheduledBackup CR.
backup.destinationPathstringObject store path. S3/MinIO: s3://my-bucket/cnpg/cluster-a; GCS: gs://my-bucket/cnpg/cluster-a.
backup.endpointURLstringOverride the storage endpoint for MinIO, Ceph RGW, etc. Leave blank for AWS S3, GCS, or Azure Blob.
backup.credentialsSecretRef.namestringSecret with storage credentials. S3/MinIO: ACCESS_KEY_ID + ACCESS_SECRET_KEY. GCS: APPLICATION_CREDENTIALS (service-account JSON). Azure: AZURE_STORAGE_ACCOUNT + AZURE_STORAGE_KEY.
backup.credentialsSecretRef.namespacestring
backup.schedulestring"0 0 2 * * *"6-field cron (sec min hr dom mon dow). Default: daily at 02:00 UTC.
backup.retentionPolicystring"30d"Barman retention window for WAL and base backups (e.g. 7d, 30d, 90d).

managedRoles[]

Cluster-wide PostgreSQL roles managed declaratively — platform team owns this array (no SSA coordination with tenant-database). Each role gets an ESO password generator, a stable ExternalSecret (refreshInterval: "0" — not auto-rotated), and a PushSecret to Vault at database-clusters/{clusterName}/roles/{name}/creds.

To change a managed role's password: delete the {clusterName}-{name}-creds Secret. CNPG regenerates it and runs ALTER ROLE with the new value.

FieldTypeDefaultDescription
managedRoles[].namestring (required)PostgreSQL role name.
managedRoles[].ensurestring"present"present or absent.
managedRoles[].loginbooleantrue
managedRoles[].superuserbooleanfalse
managedRoles[].createdbbooleanfalse
managedRoles[].createrolebooleanfalse
managedRoles[].inheritbooleantrueInherit privileges from inRoles memberships.
managedRoles[].replicationbooleanfalse
managedRoles[].bypassrlsbooleanfalseBypass row-level security policies.
managedRoles[].connectionLimitinteger-1Max concurrent connections. -1 = unlimited.
managedRoles[].inRolesarray<string>[]Predefined or custom parent roles (e.g. pg_read_all_data, pg_monitor).

bootstrapFrom

How the cluster is initialized. Default (initdb) creates a fresh empty cluster.

TypeUse case
initdb (default)Fresh cluster — no existing data to import
import-microservicesMigrate one database from an external PostgreSQL; roles NOT copied
import-monolithMigrate multiple databases and their owners from a legacy shared instance

Both import types use CNPG's built-in pg_dump-based import. The source PostgreSQL must be reachable from the cluster during bootstrap. The import is a one-time operation.

FieldTypeDefaultDescription
bootstrapFrom.typestring"initdb"initdb, import-microservices, or import-monolith.
bootstrapFrom.sourceHoststringSource PostgreSQL hostname or IP.
bootstrapFrom.sourcePortinteger5432
bootstrapFrom.sourceDatabasestringSingle database to import (import-microservices only).
bootstrapFrom.sourceDatabasesarray<string>[]Databases to import (import-monolith only).
bootstrapFrom.sourceRolesarray<string>[]Additional roles to import beyond database owners (import-monolith only).
bootstrapFrom.sourceUserstring"postgres"Source superuser name.
bootstrapFrom.sourceSSLModestring"require"One of disable, allow, prefer, require, verify-ca, verify-full.
bootstrapFrom.sourcePasswordSecretRef.namestringSecret with key password holding the source superuser password.
bootstrapFrom.sourcePasswordSecretRef.namespacestring

Required discovery label

Every XPlatformDatabaseCluster that should be available for XTenantDatabase auto-assignment must have this label in its manifest:

metadata:
labels:
wxops.cloud/managed-by: platform-database-clusters

Crossplane's composite reconciler ignores metadata.labels — the composition cannot set discovery labels automatically. Without this label, XTenantDatabase with tier: shared will not discover the cluster for pool assignment.

Examples

Shared dev cluster (standard OLTP)

apiVersion: platform.wxops.cloud/v1alpha1
kind: XPlatformDatabaseCluster
metadata:
name: shared-dev-pool
labels:
wxops.cloud/managed-by: platform-database-clusters
spec:
parameters:
clusterName: shared-dev-pool
namespace: cnpg-system
instances: 1
postgresVersion: 16
storageSize: 50Gi
shared: true
environment: dev
enablePooler: true
monitoring:
enablePodMonitor: true
backup:
enabled: true
destinationPath: s3://platform-backups/cnpg/shared-dev-pool
schedule: "0 0 3 * * *"
retentionPolicy: "7d"
credentialsSecretRef:
name: s3-backup-credentials
namespace: cnpg-system

Vector cluster (pgvector for AI workloads)

apiVersion: platform.wxops.cloud/v1alpha1
kind: XPlatformDatabaseCluster
metadata:
name: vector-dev
labels:
wxops.cloud/managed-by: platform-database-clusters
spec:
parameters:
clusterName: vector-dev
namespace: cnpg-system
instances: 1
postgresVersion: 16
storageSize: 100Gi
shared: false # dedicated clusters per vector workload
environment: dev
enablePooler: true
postgresConfig:
tuning:
sharedBuffers: "2GB"
workMem: "64MB" # vector index scans are memory-intensive
effectiveCacheSize: "6GB"
randomPageCost: "1.1" # SSD-backed PVC
maintenanceWorkMem: "1GB" # HNSW index builds use this heavily
extraParameters:
max_parallel_workers_per_gather: "4" # parallel HNSW scans
track_io_timing: "on"
monitoring:
enablePodMonitor: true

Tenants then enable pgvector when creating a database on this cluster:

apiVersion: platform.wxops.cloud/v1alpha1
kind: XTenantDatabase
metadata:
name: ai-app-embeddings
labels:
wxops.cloud/tenant-database: "true"
spec:
parameters:
clusterRef: vector-dev
clusterNamespace: cnpg-system
dbName: embeddings
owner: rocket-team
extensions:
- vector # pgvector — similarity search
- uuid-ossp # UUID generation for embedding IDs

Time-series cluster (TimescaleDB)

apiVersion: platform.wxops.cloud/v1alpha1
kind: XPlatformDatabaseCluster
metadata:
name: timeseries-prod
labels:
wxops.cloud/managed-by: platform-database-clusters
spec:
parameters:
clusterName: timeseries-prod
namespace: cnpg-system
instances: 3 # HA for production time-series
storageSize: 500Gi
shared: false
environment: prod
enablePooler: true
poolMode: session # TimescaleDB background jobs use session-level state
postgresConfig:
tuning:
sharedBuffers: "4GB"
workMem: "32MB"
maxWalSize: "4GB"
checkpointCompletionTarget: "0.9"
extraParameters:
timescaledb.max_background_workers: "8"
backup:
enabled: true
destinationPath: s3://platform-backups/cnpg/timeseries-prod
retentionPolicy: "90d"
credentialsSecretRef:
name: s3-backup-credentials
namespace: cnpg-system
monitoring:
enablePodMonitor: true
readReplica:
enablePooler: true
poolerInstances: 2 # reporting queries go to read replica

Production HA cluster with managed roles

apiVersion: platform.wxops.cloud/v1alpha1
kind: XPlatformDatabaseCluster
metadata:
name: shared-prod-pool
labels:
wxops.cloud/managed-by: platform-database-clusters
spec:
parameters:
clusterName: shared-prod-pool
namespace: cnpg-system
instances: 3
storageSize: 200Gi
shared: true
environment: prod
enablePooler: true
poolMode: transaction
postgresConfig:
tuning:
sharedBuffers: "8GB"
effectiveCacheSize: "24GB"
workMem: "32MB"
maxConnections: 200
statementTimeout: "60s"
idleInTransactionSessionTimeout: "10min"
logMinDurationStatement: "500"
managedRoles:
- name: readonly
login: true
inRoles:
- pg_read_all_data
- name: monitor
login: true
inRoles:
- pg_monitor
monitoring:
enablePodMonitor: true
backup:
enabled: true
destinationPath: s3://platform-backups/cnpg/shared-prod-pool
schedule: "0 0 2 * * *"
retentionPolicy: "30d"
credentialsSecretRef:
name: s3-backup-credentials
namespace: cnpg-system

The Bigger Picture — Inspiration

The multi-modal database vision behind XPlatformDatabaseCluster is not a novel idea — it reflects a growing consensus in the database industry that PostgreSQL has transcended its relational database origins and become a full data platform.

Two pieces shaped this direction and are worth reading for anyone on the platform team who wants to understand why the architecture is built this way:


Postgres is eating the database world — Ruohang Feng (creator of Pigsty)

The core argument: PostgreSQL has an extension ecosystem of 1,000+ packages that progressively replaces the need for separate specialized database systems. Every time an organization adopts InfluxDB for time-series, Pinecone for vectors, or Neo4j for graphs, they add a new operations burden — a new backup model, a new credential system, a new monitoring exporter, a new on-call runbook. PostgreSQL extensions give you the same specialist capability inside the engine you already operate.

The article maps the specialized database market directly onto the PostgreSQL extension landscape:

Specialized systemPostgreSQL equivalent
Pinecone, Milvus, Chromapgvector, pg_embedding
InfluxDB, Prometheus TSDBtimescaledb, pg_timeseries
Neo4jApache AGE, pgRouting
Elasticsearch, Solrpg_search (ParadeDB), tsvector
MongoDBJSONB + jsonschema
Redis (cache/queue)PGMQ, pg_ivm, LISTEN/NOTIFY
ClickHouse (analytics)pg_mooncake, Citus, pg_analytics

The thesis is not that PostgreSQL is always the right tool — it is that the operational cost of running PostgreSQL well is already paid, and extensions allow that cost to cover far more workload types than most teams realize.


Postgres is NOT A DATABASE — YouTube

A talk that reframes how to think about PostgreSQL: not as a relational database system, but as an extensible data platform — a framework for building specialized data engines. The extension architecture (shared libraries, custom types, custom operators, custom index AMs, custom FDWs) is what makes PostgreSQL's breadth possible. Each extension is a first-class citizen that integrates at the query planner and executor level, not just as a shim around an external service.


What this means for W'xOps

XPlatformDatabaseCluster is the operational layer that makes this philosophy concrete. When the platform team ships a new PostgreSQL extension image, they do not introduce a new database system — they add a new cluster profile to an infrastructure that already has pooling, backups, monitoring, Vault credential seeding, and ArgoCD lifecycle management fully working.

A tenant's XTenantDatabase claim enables vector search, time-series compression, or graph traversal by specifying extensions: [vector] or extensions: [timescaledb]. The infrastructure underneath does not change. The on-call runbook does not change. The Vault path schema does not change.

This is the compounding advantage: the platform's investment in one well-operated PostgreSQL stack compounds across every workload type the team adds, rather than fragmenting into N separate database systems each requiring N separate operator teams.

See also

  • XTenantDatabase — how tenants provision databases on platform clusters
  • Crossplane APIs — package catalog, OCI installation, and shared-pool diagram