Skip to main content
Template version 2.0.0 is a breaking change, and one of the changes is a data-loss hazard.
  • The template no longer creates a GVC. It deploys into the GVC you install into. The gvc.name and gvc.locations values are gone; locations moved to a top-level locations list.
  • Never upgrade a 1.x release onto 2.0.0 in place. A 1.x release owns the GVC it created, and Helm deletes what a chart stops declaring — the upgrade destroys that GVC and every workload, volume set and identity inside it. Migrate to a new release instead: Migrating from 1.x.
  • Replication never worked on 1.x. The pinned PostgreSQL 17.11 image does not allow Spock’s output plugin, so no replication slot could be created and every node silently accepted writes that never left it. 2.0.0 fixes this for new installs; an existing 1.x cluster needs a one-time setting change.

Overview

pgEdge is an active-active distributed PostgreSQL cluster using Spock multi-master replication. Every node accepts both reads and writes simultaneously, and data written to any node replicates to all others automatically. The cluster spans multiple geographic locations with configurable replicas per location, providing a globally distributed, fault-tolerant database with no single point of failure. Database credentials are not template values. pgEdge reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release.

Architecture

  • pgEdge — Stateful workload running PostgreSQL 17 with the Spock extension. All nodes are active writers connected in a full-mesh replication ring. Each replica gets its own persistent volume.
  • pgcat — Connection pooler providing a single virtual endpoint for applications. Sends writes to one designated node and spreads reads across the others.
  • Spock — Multi-master logical replication extension included in the pgEdge image. Handles cross-node replication with last-update-wins conflict resolution.

What Gets Created

  • Stateful pgEdge Workload — PostgreSQL 17 with the Spock extension. One set of replicas per configured location, each replica individually addressable through replica-direct DNS.
  • Standard pgcat Workload — Connection pooler that routes application traffic to pgEdge nodes. Runs pgcat.minReplicas to pgcat.maxReplicas replicas in each configured location and autoscales on RPS.
  • Cron Backup Workload (optional) — Runs pg_dump on a schedule and uploads the result to AWS S3 or GCS. Suspended in every location except the first one you configure.
  • Volume Set — ext4 general-purpose SSD volumes with 7-day snapshots and a final snapshot on deletion. One volume per replica.
  • Identity & Two Policies — An identity bound to the workloads, with reveal on this release’s secrets and on your credentials secret, plus view on the one GVC you install into so each node can confirm at boot that the GVC really has every location you listed. When backup is enabled, the identity also carries the cloud account and IAM policy.
  • Secrets — An opaque secret holding the pgEdge node startup script, an opaque secret holding the pgcat startup script, and, only when backup is enabled, a dictionary secret holding the backup destination. Your credentials secret is created by you and is never part of the release.
This template does not create a GVC. Every resource lands in the GVC you install into, so cpln workload exec, cpln logs and uninstalling all work against that GVC, and uninstalling can never delete it. Nothing runs in a GVC location you did not list in locations.

Prerequisites

A GVC must already exist, and it must contain every location you list in locations. The requirement is one-directional — the GVC may have more locations than you list, and nothing pgEdge-related runs in those. Check what a GVC has before installing:
The locations are under spec.staticPlacement.locationLinks. To add a missing one:
Every workload in a GVC runs in every location that GVC has, so add locations to a shared GVC deliberately.
A location the GVC does not have is not caught at install time. The install succeeds — the platform does not validate it — and the pgEdge containers then refuse to initialize, restarting with this in the logs:
A node that has already initialized logs a WARNING instead and keeps serving, so this check can never take down a running cluster. Read it with a server-side filter:
One dictionary secret must exist before you install. These are the credentials you type into every connection string, so they are not values — a value would leave them in the Helm release.
Set postgres.credentialsSecretName to the name you used. Secret names are org-wide, so give each release its own. pgcat uses the same password for its admin console, replacing the fixed pgcat_admin password earlier versions shipped.
Create the secret before installing, or the deployment wedges silently. The template refuses to render when postgres.credentialsSecretName is blank, but a name pointing at a secret that does not exist installs “successfully” and then never starts. The container never runs, so cpln logs returns zero lines — there is nothing to log. The one place the reason appears is status.versions[].message:
Use get-deployments — plain cpln workload get has no versions key. Creating the missing secret repairs it on its own in roughly 5.5 to 10.5 minutes, or run cpln workload force-redeployment RELEASE_NAME-pgedge --gvc GVC_NAME to clear it in about 90 seconds.
For backups you also need an AWS or GCP cloud account and a storage bucket. See Backup.

Installation

To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Migrating from 1.x

Template versions through 1.1.1 created their own GVC, so that GVC is part of the 1.x release’s manifest. Version 2.0.0 does not declare it — and Helm deletes what a chart stops declaring.
An in-place upgrade from 1.x to 2.0.0 destroys the cluster. Measured with the chart’s guard removed: the upgrade deleted the GVC and every workload, volume set and identity inside it in 6 seconds, and reported that the release had been upgraded successfully. Reading the GVC back afterwards returned 404. The volume set holds your data.The chart ships a render-time refusal so this cannot happen by accident: any leftover gvc key in your values aborts the upgrade before a single API call is made, leaving your cluster untouched and running.
The 1.0.x-era postgres.username, postgres.password and postgres.database values are refused at render too, with their own message — so pasting an old values file into a 2.0.0 install produces an error rather than a silent misconfiguration.
The guard cannot cover one case: an upgrade run with no values at all. A 1.x release installed on pure defaults has no gvc key for the chart to see, so nothing fires and the deletion proceeds. Do not run an upgrade of a 1.x release against the 2.0.0 chart under any circumstances — migrate to a new release instead.
1

Back up the old cluster

Use backup.enabled on the 1.x release, or take a manual dump against a node directly:
If replication on the old cluster is dead — which it is on every 1.x install that never applied the fix below — nodes may hold different data. Compare row counts per node before deciding which one to dump, or reconcile them first with the replication fix.
2

Choose the GVC for the new release

Create or pick a GVC and make sure it has exactly the locations you intend to list in locations. See Prerequisites.
3

Create the credentials secret and install 2.0.0 as a NEW release

Use a different release name: secret names are org-wide, so a same-named release collides with the 1.x one even in another GVC.Coming from 1.0.2 or earlier, the credentials were plain values then (postgres.username, postgres.password, postgres.database), and the template shipped a working default password that was published in the public template repository. Choose a new password now and put it in the dictionary secret — a 2.0.0 install creates its roles from that secret on a fresh data directory, so this is the moment to change it, and you are pointing applications at a new endpoint at cutover anyway.Do not carry the published 1.0.x default forward. Restoring the dump does not depend on the old cluster’s password.
4

Restore and cut over

Restore the dump into the new cluster (see Restoring a Backup), then point your applications at the new pooled endpoint, NEW_RELEASE-pgcat.NEW_GVC.cpln.local:5432.
5

Uninstall the old release against the GVC you installed it into

Not the GVC it created — the GVC you passed at install time is where Helm tracks the release, and uninstalling from there takes the created GVC with it.

Switching Replication On for an Existing 1.x Cluster

PostgreSQL 17.11 ships output_plugin_libraries = 'pgoutput, test_decoding', and Spock’s output plugin is not on that list. On every template version through 1.1.1 this means no replication slot could ever be created: each subscription sits at down, each node accepts writes that never leave it, and every status surface still reports healthy. The reason appears in the node’s own PostgreSQL server log, not in cpln logs:
Version 2.0.0 writes the setting into postgresql.conf during initdb, so a fresh 2.0.0 install needs nothing. An existing data directory keeps the old setting, so if you are staying on 1.x while planning the migration, apply it by hand on every node, connecting to each node directly rather than through pgcat:
The value must be unquoted here. ALTER SYSTEM adds the quoting for you; quoting it yourself stores a single bogus plugin literally named "pgoutput, test_decoding, spock_output" and the error persists. This differs from the postgresql.conf form, where the whole list is one quoted string. Confirm with SHOW output_plugin_libraries; — the output must contain no quotation marks.
Subscriptions move to replicating within seconds of the reload. Check on every node:
Do not restart a 1.x node to apply this — pg_reload_conf() is the whole procedure. The pre-2.0.0 startup routine drops every replication slot that is momentarily inactive, and it does so on every peer it can reach, not just locally. So a single node restarting drops the slots its peers hold for it; the subscription rows survive, nothing recreates them, and the mesh does not heal on its own. Restarting one location at a time is no safer than restarting all of them. This is fixed in 2.0.0.

Configuration

The default values.yaml for this template:

Locations and Replicas

Each entry pairs a location with a replica count. Every location listed must exist in the GVC you install into; extra GVC locations are ignored. Listing the same location twice is rejected at render — duplicates would produce duplicate Spock node names. The first entry is special: its replica-0 is the node pgcat sends writes to, and it is the only location the backup cron runs in. Set multiZone: true to spread replicas across availability zones within each location. Verify your selected locations support multiple availability zones before enabling.
GVC locations you did not list show as red in the console, with This workload location is deactivated because maxScale is set to 0. That is the mechanism that keeps a shared GVC safe — it is what a healthy install looks like, not a fault.

Volume Set

Set the initial storage capacity (minimum 10 GiB). Optionally enable autoscaling to expand as data grows:

Internal Access

Control which workloads can connect to pgEdge and pgcat. Neither workload is exposed publicly, and the template has no public access option.
  • same-gvc — Allow access from all workloads in the same GVC.
  • same-org — Allow access from all workloads in the org.
  • workload-list — Allow access only from the workloads you list, plus this release’s own workloads, which the template adds for you.
This list is not only about client traffic, and version 2.0.1 is the first version that gets it right. The same internal firewall governs Spock replication between the pgEdge nodes, pgcat’s connections to every node, and the backup job’s connection to replica-0. Under 2.0.0 a workload-list naming only your applications cut the cluster off from itself, and both failures were silent:
  • Every node kept accepting writes that never replicated anywhere, while each replica still reported ready: true.
  • Every scheduled backup uploaded an empty file and reported success, with pg_dumpall: error: ... server closed the connection unexpectedly visible only in the job’s own log.
Version 2.0.1 adds this release’s pgEdge and pgcat workloads to the list automatically, and the backup workload as well when backup.enabled is true. List only your own client workloads — anything of the release’s you add by hand is de-duplicated rather than repeated. If you are on 2.0.0 with workload-list, upgrade to 2.0.1 and then verify replication with SELECT subscription_name, status FROM spock.sub_show_status(); on every node.

pgcat Pool Modes

pgcat multiplexes application connections into a smaller pool of real database connections, reducing overhead and protecting Postgres from connection exhaustion. defaultPoolSize sets how many real Postgres connections pgcat maintains per pool.

pgcat Read and Write Routing

pgcat parses each query, sends writes to replica-0 of your first configured location, and sends reads to the other nodes. A single-node cluster has no other node, so that one node serves reads as well — otherwise every SELECT would fail with AllServersDown. This switches on node count alone and needs no configuration.
Routing is decided by statement shape, not by what the statement does. A data-modifying CTE such as WITH ins AS (INSERT ... RETURNING *) SELECT * FROM ins is classified as a read and routed to a replica. Every node accepts writes, so the write still succeeds — but “writes go to the primary” is a heuristic, not a guarantee. Send anything whose placement matters through a connection you know reaches the node you intend.
pgcat builds its configuration at container start. Its config is a TOML file, and a cpln:// secret reference inside a file is never resolved — the platform only resolves those for environment variables. So rather than rendering the password into the file, the template mounts a startup script that assembles the config from the secret at boot. Nothing changes in how you configure pgcat.

Connecting

Connect through pgcat for all application traffic. Use the fully-qualified .GVC_NAME.cpln.local form — the bare workload name does not resolve reliably from every workload type. Connect to a node directly when you need to control which node executes a statement — schema changes, per-node verification and reconciliation all require a direct connection.

Schema Changes (DDL)

Spock replicates row-level changes (INSERT, UPDATE, DELETE) automatically. DDL does not replicate. A plain CREATE TABLE or ALTER TABLE applies only to the node you ran it on; the other nodes never learn about it, and rows written into that table on one node cannot be applied on a node where it does not exist.
Every table must have a PRIMARY KEY. The template adds each new table to the default replication set automatically, and that set replicates UPDATE and DELETE, which Spock cannot do without a key. A table without one does not merely fail to replicate — the CREATE TABLE itself is rejected:

Creating a Table

The simplest correct procedure is to run the same CREATE TABLE on every node. The auto-add trigger fires locally on each one, so the table ends up in the default replication set everywhere and DML replicates in all directions:
For larger clusters you can broadcast the DDL instead — but it takes two steps, and the second one runs on the other nodes, not on the node that broadcast:
Step 2 is needed because Spock suppresses event triggers while applying replicated changes, so the auto-add trigger fires only on the node that called replicate_ddl. Running step 2 on that node fails with duplicate key value violates unique constraint "replication_set_table_pkey", and skipping it strands writes made on the other nodes — outbound filtering happens where the write lands. Confirm on every node:
Each node must return a row.

Other DDL

ALTER TABLE and DROP TABLE follow the same rule — apply on every node, or broadcast once:

Primary Keys

Use uuid primary keys instead of serial/bigserial. Each node maintains its own sequence, so auto-increment integers will collide when the same ID is generated on multiple nodes simultaneously:

Restarts and Replication Health

An upgrade, or anything else that restarts the pgEdge tier, restarts every replica in every location at once — nothing serializes a rolling restart on a stateful workload. Treat it as a planned write interruption.
  • Writes are unavailable for about 60 seconds. Measured across a three-location simultaneous restart, with one write per second flowing through pgcat.
  • pgcat bans a backend for a further 60 seconds after a failed health check, so queries can keep failing with AllServersDown for up to a minute after the nodes themselves are back.
  • Readiness is not a serving signal here. The workload can report ready before writes actually succeed again.
The mesh reconciles itself afterwards without intervention: on boot each node keeps replication slots that a subscription still owns, drops only genuinely orphaned ones, skips the cleanup entirely while any peer is unreachable, and recreates any subscription whose slot has gone missing. A deliberately broken subscription healed unattended in about two minutes, and a cluster survived three consecutive simultaneous restarts intact.
Self-repair restores replication, not history. When a broken subscription is rebuilt, replication resumes from the current position — rows written while it was down are not backfilled. A node that took writes while it was cut off needs those rows reconciled by hand.
Check replication health on any node:
Every row must read replicating. On a healthy cluster of N nodes, each node has N−1 subscriptions and N−1 replication slots.

Backup

When backup.enabled is true, a cron workload runs pg_dump on the configured schedule. Because every pgEdge node holds a full copy of the data, the backup job connects to replica-0 of the first configured location — and runs in that location only, however many locations the GVC has. Keep backup.schedule quoted in your values file. Any valid cron expression works, including one beginning with * such as "*/30 * * * *".
The backup job connects over the internal network like any other client, so it is subject to Internal Access. Version 2.0.1 adds the backup workload to a workload-list automatically when backup.enabled is true; on 2.0.0 it was omitted, and every run uploaded an empty file while reporting success.

AWS S3 Prerequisites

  1. Create your S3 bucket and note its name and region. Set backup.aws.bucket and backup.aws.region.
  2. If you do not have a Cloud Account set up, refer to the docs to Create a Cloud Account. Note the cloud account name.
  3. Create a new AWS IAM policy with the following JSON (replace YOUR_BUCKET_NAME):
Version 1.1.1 narrows AWS backup permissions. This version removes aws::ReadOnlyAccess from the backup identity. That AWS managed policy granted read access to every bucket in your AWS account and contains no write actions at all, so it was never carrying the backup itself — but it was silently supplying any read action your own bucket-scoped policy happened to omit.Update your IAM policy to the full action list below. If it already matches, no action is needed. The identity now carries cpln-connector and your bucket-scoped policy only, which is strictly narrower than before.
  1. Set backup.aws.cloudAccountName to your cloud account name.
  2. Set backup.aws.policyName to the policy created in step 3.

GCS Prerequisites

  1. Create your GCS bucket and note its name. Set backup.gcp.bucket.
  2. If you do not have a Cloud Account set up, refer to the docs to Create a Cloud Account. Note the cloud account name.
  3. Add the Storage Object Admin role (roles/storage.objectAdmin) to the GCP service account created by the cloud account — that is what the chart binds.
  4. Set backup.gcp.cloudAccountName to your cloud account name.

Restoring a Backup

AWS S3:
GCS:

Important Notes

  • Never upgrade a 1.x release onto 2.0.0 in place — it deletes the GVC the 1.x chart created and everything in it. Migrate to a new release: Migrating from 1.x.
  • The GVC must contain every location you list, and may contain more. A missing location is not caught at install: the pgEdge container exits with FATAL: locations declared in values are not in GVC .... An already-initialized node logs a WARNING instead and keeps serving.
  • Shrinking the GVC’s location list under a running cluster leaves every node logging that warning on each restart — shrink locations in your values at the same time.
  • With internal_access.type: workload-list, list only your clients. The list also governs replication between the nodes and the backup job’s connection, so a clients-only list silently breaks both. Version 2.0.1 adds the release’s own workloads for you — see Internal Access.
  • Release names must be unique per org — secrets are org-wide, so two releases with the same name collide even in different GVCs.
  • Minimum replicas — Use at least 3 replicas per location for production to survive a node loss within a location.
  • Conflict resolution — Concurrent writes to the same row from different nodes are resolved by last-update-wins based on commit timestamp. For workloads requiring stronger consistency, route writes for a given entity to a single node using application-level logic.
  • DDL does not replicate — create every table on every node, and give every table a PRIMARY KEY or CREATE TABLE fails outright.
  • multiZone — Verify your selected locations support multiple availability zones before enabling.

External References

pgEdge Documentation

Official pgEdge documentation

Spock Documentation

Spock multi-master replication extension

pgcat Documentation

pgcat connection pooler

pgEdge Template

View the source files, default values, and chart definition