Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: manage `synchronized_standby_slots`
### Is there an existing issue already for this feature request/idea?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.

### What problem is this feature going to solve? Why should it be added?

When using logical replication failover for PG17, the `synchronized_standby_slots` needs to be set to the standby physical replication slots, an incorrect specification can lead to logical replication being halted. This is difficult to manage manually when a failover occurs and is best managed by the operator

related documentation https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNCHRONIZED-STANDBY-SLOTS

### Describe the solution you'd like

On failover, update `synchronized_standby_slots` to the physical replication of the new standby instance(s)

### Describe alternatives you've considered

There aren't any alternatives, the usage of pg_failover_extension presents the same issue with `standby_slots_name`

### Additional context

_No response_

### Backport?

Yes

### Are you willing to actively contribute to this feature?

Yes

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Discussion:
@ardentperf: I believe pg_failover_slots still works with pg17, and should automatically manage logical slots. It may provide a viable alternative until native support for the new pg17 feature is contributed.

https://www.enterprisedb.com/docs/pg_extensions/pg_failover_slots/@quajones: @ardentperf the pg_failover_slots extension does manage the logical slots however the documentation does not seem to infer that the field [standby_slots_name](https://github.com/EnterpriseDB/pg_failover_slots?tab=readme-ov-file#pg_failover_slotsstandby_slot_names) is automatically managed by this extension. The need for this is to ensure that, on failover, changes to all physical replication slots are flushed before being pushed to subscribers of the logical slot@gdsantoro: Hi!

In case it's useful for anyone following this thread: I recently opened [issue #7866](https://github.com/cloudnative-pg/cloudnative-pg/issues/7866), which includes a working example and some documentation suggestions for configuring `pg_failover_slots` in a CloudNativePG cluster.

It addresses some of the challenges I encountered that may be relevant here too.@mnencia: Hi all,

Here’s an implementation proposal for the high availability logical slot synchronization feature as discussed in this issue:

---

**Implementation Plan:**

1. **API Addition:**  
   Add a new boolean field to the CRD:  
   `spec.replicationSlots.highAvailability.synchronizeLogicalDecoding`  
   When enabled, this triggers the operator to manage logical decoding slot synchronization for HA clusters.

2. **Synchronization Logic:**  
   - If the `pg_failover_slots` extension is loaded, the operator populates the `pg_failover_slots.standby_slot_names` configuration.
   - For PostgreSQL 17+, if the `sync_replication_slots` GUC is enabled, the operator updates the `synchronized_standby_slots` GUC as needed.
   - The operator ensures `hot_standby_feedback` is enabled, as it is required for both mechanisms.

3. **Webhook Validation:**  
   A validating webhook will prevent enabling `synchronizeLogicalDecoding` unless:
   - At least one of the following is true:
     - The `pg_failover_slots` extension is loaded.
     - (PG 17+) The `sync_replication_slots` GUC is enabled.
   - The `hot_standby_feedback` Postgres parameter is enabled.
   If these conditions are not met, the webhook will reject the configuration and provide a clear error message.

4. **Reconciliation:**  
   On failover or topology changes, the operator will recalculate and update the relevant configuration fields according to the current set of physical replication slots.

5. **Documentation:**  
   The documentation will be updated to reflect the new field and its requirements, including usage examples for both PG < 17 and PG 17+.

**Backward compatibility:**  
This feature is opt-in and does not affect existing clusters unless explicitly enabled.

---

Feedback and suggestions are welcome!  
/cc @gbartolini@gdsantoro: Thanks for the update - your proposal makes sense to me overall, but I’d like to double-check my understanding of the slot synchronization logic by rephrasing the scenarios below.

Scenarios:

1. **PostgreSQL version <= 16**:

    synchronization of logical replication slots must be handled via the `pg_failover_slots` extension

2. **PostgreSQL version >= 17**:

    synchronization of logical replication slots can be managed in two mutually exclusive ways:

    * by enabling and configuring the `pg_failover_slots` extension
    * or by setting the `sync_replication_slots` GUC to `true`, in which case the operator will handle the appropriate configuration of the `synchronized_standby_slots` GUC

What I’m unclear about is **what happens if both are enabled at the same time**?

Specifically, what’s the expected behavior (or failure mode) if someone both enables `pg_failover_slots` and sets `sync_replication_slots: true`?

Knowing the expected behavior would help avoid misconfiguration or unintended side effects.@gdsantoro: I'm not sure if this is the best place to raise UX-related feedback - let me know if I should move it elsewhere.

That said, there's one aspect of CNPG that I find counter-intuitive from a UX perspective - specifically, the discontinuity in how PostgreSQL configuration is managed.

Here are a few examples of behaviors that I find confusing:

1. You can configure PostgreSQL via `spec.postgresql.parameters` in the `Cluster` object. However, some configuration values can be implicitly injected or overridden based on other, seemingly unrelated, fields in the `Cluster` spec. For instance, based on your proposal, setting `spec.replicationSlots.highAvailability.synchronizeLogicalDecoding: true` will implicitly inject `hot_standby_feedback: "on"` into `spec.postgresql.parameters`.

2. A similar case applies to `spec.postgresql.shared_preload_libraries`: if you explicitly include an extension in this list it gets loaded, as expected. But if you enable `pg_failover_slots` just by setting its parameters under `spec.postgresql.parameters`, the operator implicitly adds it to the shared preload libraries, even if it wasn't explicitly specified in `spec.postgresql.shared_preload_libraries`.

While I understand these are intended as quality-of-life improvements, they can be confusing from a UX point of view - especially since the final PostgreSQL configuration resulting from a `Cluster` manifest is not fully self-evident from the manifest itself.

I believe the UX could be greatly improved if these side effects were documented on a single, centrally accessible documentation page - to avoid having to jump between multiple documentation pages.@mnencia: > What I’m unclear about is **what happens if both are enabled at the same time**?
> 
> Specifically, what’s the expected behavior (or failure mode) if someone both enables `pg_failover_slots` and sets `sync_replication_slots: true`?
> 
> Knowing the expected behavior would help avoid misconfiguration or unintended side effects.

In our tests, we found that enabling both `pg_failover_slots` and setting `sync_replication_slots` to true simultaneously does not cause any issues. While this combination may not be particularly beneficial and could potentially lead to resource waste, it functions without causing failures, except for generating some errors in the log when a worker attempts to create a slot that already exists. Therefore, you can enable both settings if you wish, but it is generally advisable to choose the configuration that best fits your specific use case.
@mnencia: > 1. You can configure PostgreSQL via `spec.postgresql.parameters` in the `Cluster` object. However, some configuration values can be implicitly injected or overridden based on other, seemingly unrelated, fields in the `Cluster` spec. For instance, based on your proposal, setting `spec.replicationSlots.highAvailability.sync_replication_slots: true` will implicitly inject `hot_standby_feedback: "on"` into `spec.postgresql.parameters`.

No, it won't. If you check the pull request, the webhook prevents you from setting `synchronizeLogicalDecoding` to true unless you enable `hot_standby_feedback` and `sync_replication_slots` first.

> 2. A similar case applies to `spec.postgresql.shared_preload_libraries`: if you explicitly include an extension in this list it gets loaded, as expected. But if you enable `pg_failover_slots` just by setting its parameters under `spec.postgresql.parameters`, the operator implicitly adds it to the shared preload libraries, even if it wasn't explicitly specified in `spec.postgresql.shared_preload_libraries`.> 
> While I understand these are intended as quality-of-life improvements, they can be confusing from a UX point of view - especially since the final PostgreSQL configuration resulting from a `Cluster` manifest is not fully self-evident from the manifest itself.
> 
> I believe the UX could be greatly improved if these side effects were documented on a single, centrally accessible documentation page - to avoid having to jump between multiple documentation pages.

I wanted to mention that the https://cloudnative-pg.io/documentation/1.26/postgresql_conf/ webpage already contains many explanations regarding the configurations and their implications. If you feel that something is missing or could be clarified further, we can certainly create a specific issue or discussion to address those points and improve the content. Your feedback is valuable!@gdsantoro: Thanks for the clarifications.

> In our tests, we found that enabling both `pg_failover_slots` and setting `sync_replication_slots` to true simultaneously does not cause any issues. While this combination may not be particularly beneficial and could potentially lead to resource waste, it functions without causing failures, except for generating some errors in the log when a worker attempts to create a slot that already exists. Therefore, you can enable both settings if you wish, but it is generally advisable to choose the configuration that best fits your specific use case.

That's exactly the kind of behavior I was wondering about, it's good to know it's safe.

> No, it won't. If you check the pull request, the webhook prevents you from setting synchronizeLogicalDecoding to true unless you enable hot_standby_feedback and sync_replication_slots first.

Makes perfect sense.

> I wanted to mention that the https://cloudnative-pg.io/documentation/1.26/postgresql_conf/ webpage already contains many explanations regarding the configurations and their implications. If you feel that something is missing or could be clarified further, we can certainly create a specific issue or discussion to address those points and improve the content. Your feedback is valuable!

Thank you again, I'll have another look at the documentation and follow up with any suggestions that might help.


Solution patch:
diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt
index 1ca2965c..839186d3 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -1332,6 +1332,7 @@ switchReplicaClusterStatus
 switchoverDelay
 switchovers
 syncReplicaElectionConstraint
+synchronizeLogicalDecoding
 synchronizeReplicas
 synchronizeReplicasCache
 sys
diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go
index b7a781b5..b5d2be6a 100644
--- a/api/v1/cluster_types.go
+++ b/api/v1/cluster_types.go
@@ -1210,6 +1210,16 @@ type ReplicationSlotsHAConfiguration struct {
 	// +kubebuilder:validation:Pattern=^[0-9a-z_]*$
 	// +optional
 	SlotPrefix string `json:"slotPrefix,omitempty"`
+
+	// When enabled, the operator automatically manages synchronization of logical
+	// decoding (replication) slots across high-availability clusters.
+	//
+	// Requires one of the following conditions:
+	// - PostgreSQL version 17 or later
+	// - PostgreSQL version < 17 with pg_failover_slots extension enabled
+	//
+	// +optional
+	SynchronizeLogicalDecoding bool `json:"synchronizeLogicalDecoding,omitempty"`
 }
 
 // KubernetesUpgradeStrategy tells the operator if the user want to
diff --git a/config/crd/bases/postgresql.cnpg.io_clusters.yaml b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
index 4be1f4b2..feb6be06 100644
--- a/config/crd/bases/postgresql.cnpg.io_clusters.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
@@ -4844,6 +4844,15 @@ spec:
                           This can only be set at creation time. By default set to `_cnpg_`.
                         pattern: ^[0-9a-z_]*$
                         type: string
+                      synchronizeLogicalDecoding:
+                        description: |-
+                          When enabled, the operator automatically manages synchronization of logical
+                          decoding (replication) slots across high-availability clusters.
+
+                          Requires one of the following conditions:
+                          - PostgreSQL version 17 or later
+                          - PostgreSQL version < 17 with pg_failover_slots extension enabled
+                        type: boolean
                     type: object
                   synchronizeReplicas:
                     description: Configures the synchronization of the user defined
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index 86ed453e..08160dba 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -5108,6 +5108,19 @@ It may only contain lower case letters, numbers, and the underscore character.
 This can only be set at creation time. By default set to <code>_cnpg_</code>.</p>
 </td>
 </tr>
+<tr><td><code>synchronizeLogicalDecoding</code><br/>
+<i>bool</i>
+</td>
+<td>
+   <p>When enabled, the operator automatically manages synchronization of logical
+decoding (replication) slots across high-availability clusters.</p>
+<p>Requires one of the following conditions:</p>
+<ul>
+<li>PostgreSQL version 17 or later</li>
+<li>PostgreSQL version &lt; 17 with pg_failover_slots extension enabled</li>
+</ul>
+</td>
+</tr>
 </tbody>
 </table>
 
diff --git a/docs/src/index.md b/docs/src/index.md
index 540274a1..1c89078a 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -131,7 +131,7 @@ Additionally, the community provides images for the [PostGIS extension](postgis.
     - *Offline Import*: Direct restore from existing databases.
     - *Online Import*: PostgreSQL native logical replication via the `Subscription` resource.
 - High Availability physical replication slots, including synchronization of
-  user-defined replication slots.
+  user-defined replication slots and logical decoding failover.
 - Parallel WAL archiving and restore, ensuring high-performance data
   synchronization in high-write environments.
 - TLS support, including:
diff --git a/docs/src/logical_replication.md b/docs/src/logical_replication.md
index 5bd36027..02dd5fbe 100644
--- a/docs/src/logical_replication.md
+++ b/docs/src/logical_replication.md
@@ -12,6 +12,14 @@ connect to publications on a publisher node. Subscribers pull data changes from
 these publications and can re-publish them, enabling cascading replication and
 complex topologies.
 
+!!! Important
+    To protect your logical replication subscribers after a failover of the
+    publisher cluster in CloudNativePG, ensure that replication slot
+    synchronization for logical decoding is enabled. Without this, your logical
+    replication clients may lose data and fail to continue seamlessly after a
+    failover. For configuration details, see
+    ["Replication: Logical Decoding Slot Synchronization"](replication.md#logical-decoding-slot-synchronization).
+
 This flexible model is particularly useful for:
 
 - Online data migrations
@@ -245,7 +253,7 @@ the `Subscription` status will reflect the following:
 If an error occurs during reconciliation, `status.applied` will be `false`, and
 an error message will be included in the `status.message` field.
 
-### Removing a subscription
+### Removing a Subscription
 
 The `subscriptionReclaimPolicy` field controls the behavior when deleting a
 `Subscription` object:
@@ -274,6 +282,13 @@ spec:
 In this case, deleting the `Subscription` object also removes the `subscriber`
 subscription from the `app` database of the `king` cluster.
 
+### Resilience to Failovers
+
+To ensure that your logical replication subscriptions remain operational after
+a failover of the publisher, configure CloudNativePG to synchronize logical
+decoding slots across the cluster. For detailed instructions, see
+[Logical Decoding Slot Synchronization](replication.md#logical-decoding-slot-synchronization).
+
 ## Limitations
 
 Logical replication in PostgreSQL has some inherent limitations, as outlined in
diff --git a/docs/src/operator_capability_levels.md b/docs/src/operator_capability_levels.md
index a98993d7..4c4a4ad2 100644
--- a/docs/src/operator_capability_levels.md
+++ b/docs/src/operator_capability_levels.md
@@ -115,11 +115,11 @@ than `1`, the operator manages `instances -1` replicas, including high
 availability (HA) through automated failover and rolling updates through
 switchover operations.
 
-CloudNativePG manages replication slots for all the replicas
-in the HA cluster. The implementation is inspired by the previously
-proposed patch for PostgreSQL, called
-[failover slots](https://wiki.postgresql.org/wiki/Failover_slots), and
-also supports user defined physical replication slots on the primary.
+CloudNativePG manages replication slots for all replicas in the
+high-availability cluster. It also supports user-defined physical replication
+slots on the primary and enables logical decoding failover—natively for
+PostgreSQL 17 and later using `sync_replication_slots`, and through the
+`pg_failover_slots` extension for earlier versions.
 
 ### Service Configuration
 
diff --git a/docs/src/replication.md b/docs/src/replication.md
index f992796c..64899632 100644
--- a/docs/src/replication.md
+++ b/docs/src/replication.md
@@ -734,6 +734,78 @@ spec:
     size: 1Gi
 ```
 
+### Logical Decoding Slot Synchronization
+
+CloudNativePG can synchronize logical decoding (replication) slots across all
+nodes in a high-availability cluster, ensuring seamless continuation of logical
+replication after a failover or switchover. This feature is disabled by
+default, and enabling it requires two steps.
+
+The first step is to enable logical decoding slot synchronization:
+
+```yaml
+  # ...
+  replicationSlots:
+    highAvailability:
+      synchronizeLogicalDecoding: true
+```
+
+The second step involves configuring PostgreSQL parameters: the required
+configuration depends on your PostgreSQL version, as explained below.
+
+When enabled, the operator automatically manages logical decoding slot states
+during failover and switchover, preventing slot invalidation and avoiding data
+loss for logical replication clients.
+
+#### Behavior on PostgreSQL 17 and later
+
+For PostgreSQL 17 and newer, CloudNativePG transparently manages the
+[`synchronized_standby_slots` parameter](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNCHRONIZED-STANDBY-SLOTS).
+
+You must enable both `sync_replication_slots` and `hot_standby_feedback` in
+your PostgreSQL configuration:
+
+```yaml
+# ...
+postgresql:
+  parameters:
+    # ...
+    hot_standby_feedback: 'on'
+    sync_replication_slots: 'on'
+```
+
+Additionally, you must create the logical replication `Subscription` with the
+`failover` option enabled, for example:
+
+```yaml
+apiVersion: postgresql.cnpg.io/v1
+kind: Subscription
+# ...
+spec:
+# ...
+  parameters:
+    failover: 'true'
+# ...
+```
+
+When configured, logical WAL sender processes send decoded changes to plugins
+only after the specified replication slots confirm receiving and flushing the
+relevant WAL, ensuring that:
+
+- logical replication slots do not consume changes until they are safely
+  received by the replicas of the publisher, and
+- logical replication clients can seamlessly reconnect to a promoted standby
+  without missing data after failover.
+
+For more details on logical replication slot synchronization, see the
+PostgreSQL documentation on [Logical Replication Failover](https://www.postgresql.org/docs/current/logical-replication-failover.html).
+
+#### Behavior on PostgreSQL 16 and earlier
+
+For PostgreSQL 16 and older versions, CloudNativePG uses the
+[`pg_failover_slots` extension](https://github.com/EnterpriseDB/pg_failover_slots)
+to maintain synchronization of logical replication slots across failovers.
+
 ### Capping the WAL size retained for replication slots
 
 When replication slots is enabled, you might end up running out of disk space
diff --git a/internal/webhook/v1/cluster_webhook.go b/internal/webhook/v1/cluster_webhook.go
index ecf19a6b..e3ee3a75 100644
--- a/internal/webhook/v1/cluster_webhook.go
+++ b/internal/webhook/v1/cluster_webhook.go
@@ -209,6 +209,7 @@ func (v *ClusterCustomValidator) validate(r *apiv1.Cluster) (allErrs field.Error
 		v.validateSynchronousReplicaConfiguration,
 		v.validateLDAP,
 		v.validateReplicationSlots,
+		v.validateSynchronizeLogicalDecoding,
 		v.validateEnv,
 		v.validateManagedServices,
 		v.validateManagedRoles,
@@ -1038,25 +1039,25 @@ func (v *ClusterCustomValidator) validateConfiguration(r *apiv1.Cluster) field.E
 		}
 	}
 
-	walLogHintsValue, walLogHintsSet := r.Spec.PostgresConfiguration.Parameters[postgres.ParameterWalLogHints]
-	if walLogHintsSet {
-		walLogHintsActivated, err := postgres.ParsePostgresConfigBoolean(walLogHintsValue)
-		if err != nil {
-			result = append(
-				result,
-				field.Invalid(
-					field.NewPath("spec", "postgresql", "parameters", postgres.ParameterWalLogHints),
-					walLogHintsValue,
-					"invalid `wal_log_hints`. Must be a postgres boolean"))
-		}
-		if r.Spec.Instances > 1 && !walLogHintsActivated {
-			result = append(
-				result,
-				field.Invalid(
-					field.NewPath("spec", "postgresql", "parameters", postgres.ParameterWalLogHints),
-					r.Spec.PostgresConfiguration.Parameters[postgres.ParameterWalLogHints],
-					"`wal_log_hints` must be set to `on` when `instances` > 1"))
-		}
+	if _, fieldError := tryParseBooleanPostgresParameter(r, postgres.ParameterHotStandbyFeedback); fieldError != nil {
+		result = append(result, fieldError)
+	}
+
+	if _, fieldError := tryParseBooleanPostgresParameter(r, postgres.ParameterSyncReplicationSlots); fieldError != nil {
+		result = append(result, fieldError)
+	}
+
+	walLogHintsActivated, fieldError := tryParseBooleanPostgresParameter(r, postgres.ParameterWalLogHints)
+	if fieldError != nil {
+		result = append(result, fieldError)
+	}
+	if walLogHintsActivated != nil && !*walLogHintsActivated && r.Spec.Instances > 1 {
+		result = append(
+			result,
+			field.Invalid(
+				field.NewPath("spec", "postgresql", "parameters", postgres.ParameterWalLogHints),
+				r.Spec.PostgresConfiguration.Parameters[postgres.ParameterWalLogHints],
+				"`wal_log_hints` must be set to `on` when `instances` > 1"))
 	}
 
 	// verify the postgres setting min_wal_size < max_wal_size < volume size
@@ -1072,6 +1073,24 @@ func (v *ClusterCustomValidator) validateConfiguration(r *apiv1.Cluster) field.E
 	return result
 }
 
+// tryParseBooleanPostgresParameter attempts to parse a boolean PostgreSQL parameter
+// from the cluster specification. If the parameter is not set, it returns nil.
+func tryParseBooleanPostgresParameter(r *apiv1.Cluster, parameterName string) (*bool, *field.Error) {
+	stringValue, hasParameter := r.Spec.PostgresConfiguration.Parameters[parameterName]
+	if !hasParameter {
+		return nil, nil
+	}
+
+	value, err := postgres.ParsePostgresConfigBoolean(stringValue)
+	if err != nil {
+		return nil, field.Invalid(
+			field.NewPath("spec", "postgresql", "parameters", parameterName),
+			stringValue,
+			fmt.Sprintf("invalid `%s` value. Must be a postgres boolean", parameterName))
+	}
+	return &value, nil
+}
+
 // validateWalSizeConfiguration verifies that min_wal_size < max_wal_size < wal volume size
 func validateWalSizeConfiguration(
 	postgresConfig apiv1.PostgresConfiguration, walVolumeSize *resource.Quantity,
@@ -2070,6 +2089,62 @@ func (v *ClusterCustomValidator) validateReplicationSlots(r *apiv1.Cluster) fiel
 	return nil
 }
 
+func (v *ClusterCustomValidator) validateSynchronizeLogicalDecoding(r *apiv1.Cluster) field.ErrorList {
+	replicationSlots := r.Spec.ReplicationSlots
+	if replicationSlots.HighAvailability == nil || !replicationSlots.HighAvailability.SynchronizeLogicalDecoding {
+		return nil
+	}
+
+	if postgres.IsManagedExtensionUsed("pg_failover_slots", r.Spec.PostgresConfiguration.Parameters) {
+		return nil
+	}
+
+	pgMajor, err := r.GetPostgresqlMajorVersion()
+	if err != nil {
+		return nil
+	}
+
+	if pgMajor < 17 {
+		return field.ErrorList{
+			field.Invalid(
+				field.NewPath("spec", "replicationSlots", "highAvailability", "synchronizeLogicalDecoding"),
+				replicationSlots.HighAvailability.SynchronizeLogicalDecoding,
+				"pg_failover_slots extension must be enabled to use synchronizeLogicalDecoding with Postgres versions < 17",
+			),
+		}
+	}
+
+	result := field.ErrorList{}
+
+	hotStandbyFeedback, _ := postgres.ParsePostgresConfigBoolean(
+		r.Spec.PostgresConfiguration.Parameters[postgres.ParameterHotStandbyFeedback])
+	if !hotStandbyFeedback {
+		result = append(
+			result,
+			field.Invalid(
+				field.NewPath("spec", "postgresql", "parameters", postgres.ParameterHotStandbyFeedback),
+				hotStandbyFeedback,
+				fmt.Sprintf("`%s` must be enabled to enable "+
+					"`spec.replicationSlots.highAvailability.synchronizeLogicalDecoding`",
+					postgres.ParameterHotStandbyFeedback)))
+	}
+
+	const syncReplicationSlotsKey = "sync_replication_slots"
+	syncReplicationSlots, _ := postgres.ParsePostgresConfigBoolean(
+		r.Spec.PostgresConfiguration.Parameters[syncReplicationSlotsKey])
+	if !syncReplicationSlots {
+		result = append(
+			result,
+			field.Invalid(
+				field.NewPath("spec", "postgresql", "parameters", syncReplicationSlotsKey),
+				syncReplicationSlots,
+				fmt.Sprintf("either `%s` setting or pg_failover_slots extension must be enabled to enable "+
+					"`spec.replicationSlots.highAvailability.synchronizeLogicalDecoding`", syncReplicationSlotsKey)))
+	}
+
+	return result
+}
+
 func (v *ClusterCustomValidator) validateReplicationSlotsChange(r, old *apiv1.Cluster) field.ErrorList {
 	newReplicationSlots := r.Spec.ReplicationSlots
 	oldReplicationSlots := old.Spec.ReplicationSlots
@@ -2276,38 +2351,20 @@ func (v *ClusterCustomValidator) validatePgFailoverSlots(r *apiv1.Cluster) field
 	var result field.ErrorList
 	var pgFailoverSlots postgres.ManagedExtension
 
-	for i, ext := range postgres.ManagedExtensions {
-		if ext.Name == "pg_failover_slots" {
-			pgFailoverSlots = postgres.ManagedExtensions[i]
-		}
-	}
-	if !pgFailoverSlots.IsUsed(r.Spec.PostgresConfiguration.Parameters) {
+	if !postgres.IsManagedExtensionUsed("pg_failover_slots", r.Spec.PostgresConfiguration.Parameters) {
 		return nil
 	}
 
-	const hotStandbyFeedbackKey = "hot_standby_feedback"
-	hotStandbyFeedbackActivated := false
-	hotStandbyFeedback, hasHotStandbyFeedback := r.Spec.PostgresConfiguration.Parameters[hotStandbyFeedbackKey]
-	if hasHotStandbyFeedback {
-		var err error
-		hotStandbyFeedbackActivated, err = postgres.ParsePostgresConfigBoolean(hotStandbyFeedback)
-		if err != nil {
-			result = append(
-				result,
-				field.Invalid(
-					field.NewPath("spec", "postgresql", "parameters", hotStandbyFeedbackKey),
-					hotStandbyFeedback,
-					fmt.Sprintf("invalid `%s` value. Must be a postgres boolean", hotStandbyFeedbackKey)))
-		}
-	}
-
-	if !hotStandbyFeedbackActivated {
+	hotStandbyFeedback, _ := postgres.ParsePostgresConfigBoolean(
+		r.Spec.PostgresConfiguration.Parameters[postgres.ParameterHotStandbyFeedback])
+	if !hotStandbyFeedback {
 		result = append(
 			result,
 			field.Invalid(
-				field.NewPath("spec", "postgresql", "parameters", hotStandbyFeedbackKey),
+				field.NewPath("spec", "postgresql", "parameters", postgres.ParameterHotStandbyFeedback),
 				hotStandbyFeedback,
-				fmt.Sprintf("`%s` must be enabled to use %s extension", hotStandbyFeedbackKey, pgFailoverSlots.Name)))
+				fmt.Sprintf("`%s` must be enabled to use %s extension",
+					postgres.ParameterHotStandbyFeedback, pgFailoverSlots.Name)))
 	}
 
 	if r.Spec.ReplicationSlots == nil {
diff --git a/pkg/management/postgres/configuration.go b/pkg/management/postgres/configuration.go
index da075978..1445e086 100644
--- a/pkg/management/postgres/configuration.go
+++ b/pkg/management/postgres/configuration.go
@@ -431,6 +431,17 @@ func createPostgresqlConfiguration(
 		info.RecoveryMinApplyDelay = cluster.Spec.ReplicaCluster.MinApplyDelay.Duration
 	}
 
+	if isSynchronizeLogicalDecodingEnabled(cluster) {
+		slots := make([]string, 0, len(cluster.Status.InstanceNames)-1)
+		for _, instanceName := range cluster.Status.InstanceNames {
+			if instanceName == cluster.Status.CurrentPrimary {
+				continue
+			}
+			slots = append(slots, cluster.GetSlotNameFromInstanceName(instanceName))
+		}
+		info.SynchronizedStandbySlots = slots
+	}
+
 	config, err := plugin.CreatePostgresqlConfigurationWithPlugins(ctx, info, operationType)
 	if err != nil {
 		return "", "", err
@@ -440,6 +451,13 @@ func createPostgresqlConfiguration(
 	return file, sha, nil
 }
 
+func isSynchronizeLogicalDecodingEnabled(cluster *apiv1.Cluster) bool {
+	return cluster.Spec.ReplicationSlots != nil &&
+		cluster.Spec.ReplicationSlots.HighAvailability != nil &&
+		cluster.Spec.ReplicationSlots.HighAvailability.GetEnabled() &&
+		cluster.Spec.ReplicationSlots.HighAvailability.SynchronizeLogicalDecoding
+}
+
 // configurePostgresForImport configures Postgres to be optimized for the firt import
 // process, by writing dedicated options the override.conf file just for this phase
 func configurePostgresForImport(ctx context.Context, pgData string) (changed bool, err error) {
diff --git a/pkg/postgres/configuration.go b/pkg/postgres/configuration.go
index 66a0c109..118dcd5c 100644
--- a/pkg/postgres/configuration.go
+++ b/pkg/postgres/configuration.go
@@ -48,6 +48,12 @@ const (
 
 	// ParameterRecoveryMinApplyDelay is the configuration key containing the recovery_min_apply_delay parameter
 	ParameterRecoveryMinApplyDelay = "recovery_min_apply_delay"
+
+	// ParameterSyncReplicationSlots the configuration key containing the sync_replication_slots value
+	ParameterSyncReplicationSlots = "sync_replication_slots"
+
+	// ParameterHotStandbyFeedback the configuration key containing the hot_standby_feedback value
+	ParameterHotStandbyFeedback = "hot_standby_feedback"
 )
 
 // An acceptable wal_level value
@@ -297,6 +303,9 @@ type ConfigurationInfo struct {
 	// The synchronous_standby_names configuration to be applied
 	SynchronousStandbyNames string
 
+	// The synchronized_standby_slots configuration to be applied
+	SynchronizedStandbySlots []string
+
 	// List of additional sharedPreloadLibraries to be loaded
 	AdditionalSharedPreloadLibraries []string
 
@@ -367,6 +376,23 @@ func (e ManagedExtension) IsUsed(userConfigs map[string]string) bool {
 	return false
 }
 
+// IsManagedExtensionUsed checks whether a configuration namespace in the named extension namespaces list
+// is used in the user-provided configuration
+func IsManagedExtensionUsed(name string, userConfigs map[string]string) bool {
+	var extension *ManagedExtension
+	for _, ext := range ManagedExtensions {
+		if ext.Name == name {
+			extension = &ext
+			break
+		}
+	}
+	if extension == nil {
+		return false
+	}
+
+	return extension.IsUsed(userConfigs)
+}
+
 var (
 	// ManagedExtensions contains the list of extensions the operator supports to manage
 	ManagedExtensions = []ManagedExtension{
@@ -443,6 +469,7 @@ var (
 		"log_rotation_size":                      blockedConfigurationParameter,
 		"log_truncate_on_rotation":               blockedConfigurationParameter,
 		"pg_failover_slots.primary_dsn":          fixedConfigurationParameter,
+		"pg_failover_slots.standby_slot_names":   fixedConfigurationParameter,
 		"promote_trigger_file":                   blockedConfigurationParameter,
 		"recovery_end_command":                   blockedConfigurationParameter,
 		"recovery_min_apply_delay":               blockedConfigurationParameter,
@@ -459,6 +486,7 @@ var (
 		"ssl_prefer_server_ciphers":              fixedConfigurationParameter,
 		"stats_temp_directory":                   blockedConfigurationParameter,
 		"synchronous_standby_names":              fixedConfigurationParameter,
+		"synchronized_standby_slots":             fixedConfigurationParameter,
 		"syslog_facility":                        blockedConfigurationParameter,
 		"syslog_ident":                           blockedConfigurationParameter,
 		"syslog_sequence_numbers":                blockedConfigurationParameter,
@@ -665,6 +693,19 @@ func CreatePostgresqlConfiguration(info ConfigurationInfo) *PgConfiguration {
 		configuration.OverwriteConfig(SynchronousStandbyNames, syncStandbyNames)
 	}
 
+	if len(info.SynchronizedStandbySlots) > 0 {
+		synchronizedStandbySlots := strings.Join(info.SynchronizedStandbySlots, ",")
+		if IsManagedExtensionUsed("pg_failover_slots", info.UserSettings) {
+			configuration.OverwriteConfig("pg_failover_slots.standby_slot_names", synchronizedStandbySlots)
+		}
+
+		if info.MajorVersion >= 17 {
+			if isEnabled, _ := ParsePostgresConfigBoolean(info.UserSettings["sync_replication_slots"]); isEnabled {
+				configuration.OverwriteConfig("synchronized_standby_slots", synchronizedStandbySlots)
+			}
+		}
+	}
+
 	if info.ClusterName != "" {
 		configuration.OverwriteConfig("cluster_name", info.ClusterName)
 	}



Test patch:
diff --git a/internal/webhook/v1/cluster_webhook_test.go b/internal/webhook/v1/cluster_webhook_test.go
index 2ab0e37a..5d21839e 100644
--- a/internal/webhook/v1/cluster_webhook_test.go
+++ b/internal/webhook/v1/cluster_webhook_test.go
@@ -3686,6 +3686,126 @@ var _ = Describe("validation of replication slots configuration", func() {
 		errors := v.validateReplicationSlots(cluster)
 		Expect(errors).To(BeEmpty())
 	})
+
+	It("returns no errors when synchronizeLogicalDecoding is disabled", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: false,
+					},
+				},
+			},
+		}
+
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(BeNil())
+	})
+
+	It("returns no errors when pg_failover_slots is enabled", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: true,
+					},
+				},
+				PostgresConfiguration: apiv1.PostgresConfiguration{
+					Parameters: map[string]string{
+						"hot_standby_feedback":                     "true",
+						"pg_failover_slots.synchronize_slot_names": "name_like:%",
+					},
+				},
+			},
+		}
+
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(BeNil())
+	})
+
+	It("returns an error when Postgres version is < 17 and pg_failover_slots is not enabled", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ImageName: "postgres:16",
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: true,
+					},
+				},
+			},
+		}
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(HaveLen(1))
+		Expect(result[0].Error()).To(ContainSubstring("pg_failover_slots extension must be enabled"))
+	})
+
+	It("returns an error when Postgres version is 17 and hot_standby_feedback is disabled", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ImageName: "postgres:17",
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: true,
+					},
+				},
+				PostgresConfiguration: apiv1.PostgresConfiguration{
+					Parameters: map[string]string{
+						"sync_replication_slots": "on",
+						"hot_standby_feedback":   "off",
+					},
+				},
+			},
+		}
+
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(HaveLen(1))
+		Expect(result[0].Error()).To(ContainSubstring("`hot_standby_feedback` must be enabled"))
+	})
+
+	It("returns an error when Postgres version is 17 and sync_replication_slots is disabled", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ImageName: "postgres:17",
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: true,
+					},
+				},
+				PostgresConfiguration: apiv1.PostgresConfiguration{
+					Parameters: map[string]string{
+						"hot_standby_feedback":   "on",
+						"sync_replication_slots": "off",
+					},
+				},
+			},
+		}
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(HaveLen(1))
+		Expect(result[0].Error()).To(ContainSubstring(
+			"`sync_replication_slots` setting or pg_failover_slots extension must be enabled"))
+	})
+
+	It("returns no errors when all conditions are met", func() {
+		cluster := &apiv1.Cluster{
+			Spec: apiv1.ClusterSpec{
+				ImageName: "postgres:17",
+				ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
+					HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
+						SynchronizeLogicalDecoding: true,
+					},
+				},
+				PostgresConfiguration: apiv1.PostgresConfiguration{
+					Parameters: map[string]string{
+						"hot_standby_feedback":   "on",
+						"sync_replication_slots": "on",
+					},
+				},
+			},
+		}
+
+		result := v.validateSynchronizeLogicalDecoding(cluster)
+		Expect(result).To(BeEmpty())
+	})
 })
 
 var _ = Describe("Environment variables validation", func() {
@@ -3994,7 +4114,7 @@ var _ = Describe("Managed Extensions validation", func() {
 				},
 			},
 		}
-		Expect(v.validatePgFailoverSlots(cluster)).To(HaveLen(2))
+		Expect(v.validatePgFailoverSlots(cluster)).To(HaveLen(1))
 	})
 
 	It("should succeed if pg_failover_slots and its prerequisites are enabled", func() {