Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: Declarative support for foreign servers
### 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?

Following #4683, which adds support for declarative foreign data wrappers, we are missing a declarative way to define a foreign server.

### Describe the solution you'd like

INCOMPLETE DRAFT of proposal:

```yaml
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
  name: app
spec:
  name: app
  owner: app
  cluster:
    name: pg-b
  extensions:
  - name: postgres_fdw
    ensure: present
  fdws:
  - name: postgres_fdw
     usage:
     - app
  servers:
  - name: pg-a
    fdw: postgres_fdw
    ensure: present
#    options:
#    - name: host
#      value: foo
#    -name: dbname
#      value: app
    optionsRef:
    - name: pg-a-app
          key: host
```

### Describe alternatives you've considered

Have if under the FDW stanza.

### Additional context

_No response_

### Backport?

No

### 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:
@devc007: Hey @gbartolini,
I’m really interested in contributing and also in participating in the LFX mentorship program. I noticed that most repositories require prior contributions, and while I’ve looked through the “good first issue” label, many of them seem to be already assigned — so I’m not sure where to start.

Are there any other criteria for mentee selection in CloudNativePG? Any guidance would be really appreciated!

@EdwinaZhu: Hi @gbartolini , could you please assign this extended feat for me?@gbartolini: I am assigning this ticket to @EdwinaZhu, our first mentee. She has been doing an excellent job with the foreign data wrappers feature (PR #7942), and she has agreed to extend her mentorship to this task as well. We are targeting both features in 1.28.

Solution patch:
diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt
index 71e973ad..c9b9647a 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -165,6 +165,9 @@ ExtensionConfiguration
 ExtensionSpec
 ExtensionStatus
 ExternalCluster
+FDW
+FDWSpec
+FDWs
 FQDN
 FQDNs
 FailoverQuorum
@@ -306,6 +309,8 @@ Openshift
 OperatorCapabilities
 OperatorGroup
 OperatorHub
+OptionSpec
+OptionSpecValue
 OwnNamespace
 PDB
 PDBs
@@ -524,6 +529,7 @@ UTF
 Uncomment
 Unrealizable
 UpdateStrategy
+UsageSpec
 VLDB
 VLDBs
 VM
@@ -841,6 +847,8 @@ faq
 fastpath
 fb
 fd
+fdw
+fdws
 ffd
 fieldPath
 fieldref
@@ -854,6 +862,7 @@ firstRecoverabilityPointByMethod
 fqdn
 freddie
 fuzzystrmatch
+gRPC
 gapped
 gc
 gcc
@@ -873,7 +882,6 @@ googleCredentials
 goroutines
 gosec
 govulncheck
-gRPC
 grafana
 gzip
 hashicorp
@@ -1000,6 +1008,7 @@ lookups
 lsn
 lt
 lz
+mTLS
 macOS
 majorVersion
 majorVersionUpgradeFromImage
@@ -1038,7 +1047,6 @@ monitoringconfiguration
 mountPath
 msg
 mspan
-mTLS
 multinamespace
 mutatingwebhookconfigurations
 mutex
@@ -1443,6 +1451,7 @@ usr
 utils
 validUntil
 validatingwebhookconfigurations
+validator
 valueFrom
 viceversa
 virtualized
diff --git a/api/v1/database_types.go b/api/v1/database_types.go
index 0862d88d..9baee51c 100644
--- a/api/v1/database_types.go
+++ b/api/v1/database_types.go
@@ -173,6 +173,10 @@ type DatabaseSpec struct {
 	// The list of extensions to be managed in the database
 	// +optional
 	Extensions []ExtensionSpec `json:"extensions,omitempty"`
+
+	// The list of foreign data wrappers to be managed in the database
+	// +optional
+	FDWs []FDWSpec `json:"fdws,omitempty"`
 }
 
 // DatabaseObjectSpec contains the fields which are common to every
@@ -220,6 +224,74 @@ type ExtensionSpec struct {
 	Schema string `json:"schema,omitempty"`
 }
 
+// FDWSpec configures an Foreign Data Wrapper in a database
+type FDWSpec struct {
+	// Common fields
+	DatabaseObjectSpec `json:",inline"`
+
+	// Name of the handler function (e.g., "postgres_fdw_handler").
+	// This will be empty if no handler is specified. In that case,
+	// the default handler is registered when the FDW extension is created.
+	// +optional
+	Handler string `json:"handler,omitempty"`
+
+	// Name of the validator function (e.g., "postgres_fdw_validator").
+	// This will be empty if no validator is specified. In that case,
+	// the default validator is registered when the FDW extension is created.
+	// +optional
+	Validator string `json:"validator,omitempty"`
+
+	// Owner specifies the database role that will own the Foreign Data Wrapper.
+	// The role must have superuser privileges in the target database.
+	// +optional
+	Owner string `json:"owner,omitempty"`
+
+	// Options specifies the configuration options for the FDW
+	// (key is the option name, value is the option value).
+	// +optional
+	Options []OptionSpec `json:"options,omitempty"`
+
+	// List of roles for which `USAGE` privileges on the FDW are granted or revoked.
+	// +optional
+	Usages []UsageSpec `json:"usage,omitempty"`
+}
+
+// OptionSpec holds the name, value and the ensure field for an option
+type OptionSpec struct {
+	// Name of the option
+	Name string `json:"name"`
+
+	// Value and ensure field of the option
+	OptionSpecValue `json:",inline"`
+}
+
+// OptionSpecValue holds the value and the ensure field for an option
+type OptionSpecValue struct {
+	// Value of the option
+	Value string `json:"value"`
+
+	// Specifies whether an option should be present or absent in
+	// the database. If set to `present`, the option will be
+	// created if it does not exist. If set to `absent`, the
+	// option will be removed if it exists.
+	// +kubebuilder:default:="present"
+	// +kubebuilder:validation:Enum=present;absent
+	// +optional
+	Ensure EnsureOption `json:"ensure,omitempty"`
+}
+
+// UsageSpec configures a usage for a foreign data wrapper
+type UsageSpec struct {
+	// Name of the usage
+	Name string `json:"name"`
+
+	// The type of usage
+	// +kubebuilder:default:="grant"
+	// +kubebuilder:validation:Enum=grant;revoke
+	// +optional
+	Type string `json:"type,omitempty"`
+}
+
 // DatabaseStatus defines the observed state of Database
 type DatabaseStatus struct {
 	// A sequence number representing the latest
@@ -242,6 +314,10 @@ type DatabaseStatus struct {
 	// Extensions is the status of the managed extensions
 	// +optional
 	Extensions []DatabaseObjectStatus `json:"extensions,omitempty"`
+
+	// FDWs is the status of the managed FDWs
+	// +optional
+	FDWs []DatabaseObjectStatus `json:"fdws,omitempty"`
 }
 
 // DatabaseObjectStatus is the status of the managed database objects
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index 2d601693..65c47bb8 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -1166,6 +1166,13 @@ func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) {
 		*out = make([]ExtensionSpec, len(*in))
 		copy(*out, *in)
 	}
+	if in.FDWs != nil {
+		in, out := &in.FDWs, &out.FDWs
+		*out = make([]FDWSpec, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseSpec.
@@ -1196,6 +1203,11 @@ func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) {
 		*out = make([]DatabaseObjectStatus, len(*in))
 		copy(*out, *in)
 	}
+	if in.FDWs != nil {
+		in, out := &in.FDWs, &out.FDWs
+		*out = make([]DatabaseObjectStatus, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseStatus.
@@ -1361,6 +1373,32 @@ func (in *ExternalCluster) DeepCopy() *ExternalCluster {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FDWSpec) DeepCopyInto(out *FDWSpec) {
+	*out = *in
+	out.DatabaseObjectSpec = in.DatabaseObjectSpec
+	if in.Options != nil {
+		in, out := &in.Options, &out.Options
+		*out = make([]OptionSpec, len(*in))
+		copy(*out, *in)
+	}
+	if in.Usages != nil {
+		in, out := &in.Usages, &out.Usages
+		*out = make([]UsageSpec, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FDWSpec.
+func (in *FDWSpec) DeepCopy() *FDWSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(FDWSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *FailoverQuorum) DeepCopyInto(out *FailoverQuorum) {
 	*out = *in
@@ -1982,6 +2020,37 @@ func (in *OnlineConfiguration) DeepCopy() *OnlineConfiguration {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OptionSpec) DeepCopyInto(out *OptionSpec) {
+	*out = *in
+	out.OptionSpecValue = in.OptionSpecValue
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionSpec.
+func (in *OptionSpec) DeepCopy() *OptionSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(OptionSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OptionSpecValue) DeepCopyInto(out *OptionSpecValue) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionSpecValue.
+func (in *OptionSpecValue) DeepCopy() *OptionSpecValue {
+	if in == nil {
+		return nil
+	}
+	out := new(OptionSpecValue)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PasswordState) DeepCopyInto(out *PasswordState) {
 	*out = *in
@@ -3332,6 +3401,21 @@ func (in *Topology) DeepCopy() *Topology {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UsageSpec) DeepCopyInto(out *UsageSpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UsageSpec.
+func (in *UsageSpec) DeepCopy() *UsageSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(UsageSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VolumeSnapshotConfiguration) DeepCopyInto(out *VolumeSnapshotConfiguration) {
 	*out = *in
diff --git a/config/crd/bases/postgresql.cnpg.io_databases.yaml b/config/crd/bases/postgresql.cnpg.io_databases.yaml
index 50626148..d417b9b8 100644
--- a/config/crd/bases/postgresql.cnpg.io_databases.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_databases.yaml
@@ -161,6 +161,98 @@ spec:
                   - name
                   type: object
                 type: array
+              fdws:
+                description: The list of foreign data wrappers to be managed in the
+                  database
+                items:
+                  description: FDWSpec configures an Foreign Data Wrapper in a database
+                  properties:
+                    ensure:
+                      default: present
+                      description: |-
+                        Specifies whether an extension/schema should be present or absent in
+                        the database. If set to `present`, the extension/schema will be
+                        created if it does not exist. If set to `absent`, the
+                        extension/schema will be removed if it exists.
+                      enum:
+                      - present
+                      - absent
+                      type: string
+                    handler:
+                      description: |-
+                        Name of the handler function (e.g., "postgres_fdw_handler").
+                        This will be empty if no handler is specified. In that case,
+                        the default handler is registered when the FDW extension is created.
+                      type: string
+                    name:
+                      description: Name of the extension/schema
+                      type: string
+                    options:
+                      description: |-
+                        Options specifies the configuration options for the FDW
+                        (key is the option name, value is the option value).
+                      items:
+                        description: OptionSpec holds the name, value and the ensure
+                          field for an option
+                        properties:
+                          ensure:
+                            default: present
+                            description: |-
+                              Specifies whether an option should be present or absent in
+                              the database. If set to `present`, the option will be
+                              created if it does not exist. If set to `absent`, the
+                              option will be removed if it exists.
+                            enum:
+                            - present
+                            - absent
+                            type: string
+                          name:
+                            description: Name of the option
+                            type: string
+                          value:
+                            description: Value of the option
+                            type: string
+                        required:
+                        - name
+                        - value
+                        type: object
+                      type: array
+                    owner:
+                      description: |-
+                        Owner specifies the database role that will own the Foreign Data Wrapper.
+                        The role must have superuser privileges in the target database.
+                      type: string
+                    usage:
+                      description: List of roles for which `USAGE` privileges on the
+                        FDW are granted or revoked.
+                      items:
+                        description: UsageSpec configures a usage for a foreign data
+                          wrapper
+                        properties:
+                          name:
+                            description: Name of the usage
+                            type: string
+                          type:
+                            default: grant
+                            description: The type of usage
+                            enum:
+                            - grant
+                            - revoke
+                            type: string
+                        required:
+                        - name
+                        type: object
+                      type: array
+                    validator:
+                      description: |-
+                        Name of the validator function (e.g., "postgres_fdw_validator").
+                        This will be empty if no validator is specified. In that case,
+                        the default validator is registered when the FDW extension is created.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
               icuLocale:
                 description: |-
                   Maps to the `ICU_LOCALE` parameter of `CREATE DATABASE`. This
@@ -330,6 +422,28 @@ spec:
                   - name
                   type: object
                 type: array
+              fdws:
+                description: FDWs is the status of the managed FDWs
+                items:
+                  description: DatabaseObjectStatus is the status of the managed database
+                    objects
+                  properties:
+                    applied:
+                      description: |-
+                        True of the object has been installed successfully in
+                        the database
+                      type: boolean
+                    message:
+                      description: Message is the object reconciliation message
+                      type: string
+                    name:
+                      description: The name of the object
+                      type: string
+                  required:
+                  - applied
+                  - name
+                  type: object
+                type: array
               message:
                 description: Message is the reconciliation output message
                 type: string
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index 6ceff12c..ba0d3211 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -2426,6 +2426,8 @@ PostgreSQL cluster from an existing storage</p>
 
 - [ExtensionSpec](#postgresql-cnpg-io-v1-ExtensionSpec)
 
+- [FDWSpec](#postgresql-cnpg-io-v1-FDWSpec)
+
 - [SchemaSpec](#postgresql-cnpg-io-v1-SchemaSpec)
 
 
@@ -2724,6 +2726,13 @@ tablespace used for objects created in this database.</p>
    <p>The list of extensions to be managed in the database</p>
 </td>
 </tr>
+<tr><td><code>fdws</code><br/>
+<a href="#postgresql-cnpg-io-v1-FDWSpec"><i>[]FDWSpec</i></a>
+</td>
+<td>
+   <p>The list of foreign data wrappers to be managed in the database</p>
+</td>
+</tr>
 </tbody>
 </table>
 
@@ -2777,6 +2786,13 @@ desired state that was synchronized</p>
    <p>Extensions is the status of the managed extensions</p>
 </td>
 </tr>
+<tr><td><code>fdws</code><br/>
+<a href="#postgresql-cnpg-io-v1-DatabaseObjectStatus"><i>[]DatabaseObjectStatus</i></a>
+</td>
+<td>
+   <p>FDWs is the status of the managed FDWs</p>
+</td>
+</tr>
 </tbody>
 </table>
 
@@ -2819,6 +2835,8 @@ desired state that was synchronized</p>
 
 - [DatabaseSpec](#postgresql-cnpg-io-v1-DatabaseSpec)
 
+- [OptionSpecValue](#postgresql-cnpg-io-v1-OptionSpecValue)
+
 - [RoleConfiguration](#postgresql-cnpg-io-v1-RoleConfiguration)
 
 
@@ -3043,6 +3061,71 @@ of WAL archiving and backups for this external cluster</p>
 </tbody>
 </table>
 
+## FDWSpec     {#postgresql-cnpg-io-v1-FDWSpec}
+
+
+**Appears in:**
+
+- [DatabaseSpec](#postgresql-cnpg-io-v1-DatabaseSpec)
+
+
+<p>FDWSpec configures an Foreign Data Wrapper in a database</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>DatabaseObjectSpec</code><br/>
+<a href="#postgresql-cnpg-io-v1-DatabaseObjectSpec"><i>DatabaseObjectSpec</i></a>
+</td>
+<td>(Members of <code>DatabaseObjectSpec</code> are embedded into this type.)
+   <p>Common fields</p>
+</td>
+</tr>
+<tr><td><code>handler</code><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Name of the handler function (e.g., &quot;postgres_fdw_handler&quot;).
+This will be empty if no handler is specified. In that case,
+the default handler is registered when the FDW extension is created.</p>
+</td>
+</tr>
+<tr><td><code>validator</code><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Name of the validator function (e.g., &quot;postgres_fdw_validator&quot;).
+This will be empty if no validator is specified. In that case,
+the default validator is registered when the FDW extension is created.</p>
+</td>
+</tr>
+<tr><td><code>owner</code><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Owner specifies the database role that will own the Foreign Data Wrapper.
+The role must have superuser privileges in the target database.</p>
+</td>
+</tr>
+<tr><td><code>options</code><br/>
+<a href="#postgresql-cnpg-io-v1-OptionSpec"><i>[]OptionSpec</i></a>
+</td>
+<td>
+   <p>Options specifies the configuration options for the FDW
+(key is the option name, value is the option value).</p>
+</td>
+</tr>
+<tr><td><code>usage</code><br/>
+<a href="#postgresql-cnpg-io-v1-UsageSpec"><i>[]UsageSpec</i></a>
+</td>
+<td>
+   <p>List of roles for which <code>USAGE</code> privileges on the FDW are granted or revoked.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## FailoverQuorumStatus     {#postgresql-cnpg-io-v1-FailoverQuorumStatus}
 
 
@@ -3932,6 +4015,71 @@ possible. <code>false</code> by default.</p>
 </tbody>
 </table>
 
+## OptionSpec     {#postgresql-cnpg-io-v1-OptionSpec}
+
+
+**Appears in:**
+
+- [FDWSpec](#postgresql-cnpg-io-v1-FDWSpec)
+
+
+<p>OptionSpec holds the name, value and the ensure field for an option</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>name</code> <B>[Required]</B><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Name of the option</p>
+</td>
+</tr>
+<tr><td><code>OptionSpecValue</code><br/>
+<a href="#postgresql-cnpg-io-v1-OptionSpecValue"><i>OptionSpecValue</i></a>
+</td>
+<td>(Members of <code>OptionSpecValue</code> are embedded into this type.)
+   <p>Value and ensure field of the option</p>
+</td>
+</tr>
+</tbody>
+</table>
+
+## OptionSpecValue     {#postgresql-cnpg-io-v1-OptionSpecValue}
+
+
+**Appears in:**
+
+- [OptionSpec](#postgresql-cnpg-io-v1-OptionSpec)
+
+
+<p>OptionSpecValue holds the value and the ensure field for an option</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>value</code> <B>[Required]</B><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Value of the option</p>
+</td>
+</tr>
+<tr><td><code>ensure</code><br/>
+<a href="#postgresql-cnpg-io-v1-EnsureOption"><i>EnsureOption</i></a>
+</td>
+<td>
+   <p>Specifies whether an option should be present or absent in
+the database. If set to <code>present</code>, the option will be
+created if it does not exist. If set to <code>absent</code>, the
+option will be removed if it exists.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## PasswordState     {#postgresql-cnpg-io-v1-PasswordState}
 
 
@@ -6402,6 +6550,37 @@ in synchronous replica election in case of failures</p>
 </tbody>
 </table>
 
+## UsageSpec     {#postgresql-cnpg-io-v1-UsageSpec}
+
+
+**Appears in:**
+
+- [FDWSpec](#postgresql-cnpg-io-v1-FDWSpec)
+
+
+<p>UsageSpec configures a usage for a foreign data wrapper</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>name</code> <B>[Required]</B><br/>
+<i>string</i>
+</td>
+<td>
+   <p>Name of the usage</p>
+</td>
+</tr>
+<tr><td><code>type</code><br/>
+<i>string</i>
+</td>
+<td>
+   <p>The type of usage</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## VolumeSnapshotConfiguration     {#postgresql-cnpg-io-v1-VolumeSnapshotConfiguration}
 
 
diff --git a/docs/src/declarative_database_management.md b/docs/src/declarative_database_management.md
index 85bc01c6..b938a65d 100644
--- a/docs/src/declarative_database_management.md
+++ b/docs/src/declarative_database_management.md
@@ -256,6 +256,76 @@ Each schema entry supports the following properties:
     [`DROP SCHEMA`](https://www.postgresql.org/docs/current/sql-dropschema.html),
     [`ALTER SCHEMA`](https://www.postgresql.org/docs/current/sql-alterschema.html).
 
+## Managing Foreign Data Wrappers (FDWs) In a Database
+
+!!! Info
+    Foreign Data Wrappers (FDWs) are database-scoped objects that typically
+    require superuser privileges to create or modify. CloudNativePG provides a
+    declarative API for managing FDWs, enabling users to define and maintain them
+    in a controlled, Kubernetes-native way without directly executing SQL commands
+    or escalating privileges.
+
+CloudNativePG enables seamless and automated management of PostgreSQL foreign
+data wrappers in the target database using declarative configuration.
+
+To enable this feature, define the `spec.fdws` field with a list of FDW
+specifications, as shown in the following example:
+
+```yaml
+# ...
+spec:
+  fdws:
+  - name: postgres_fdw
+    usage:
+      - name: app
+      - type: grant
+# ...
+```
+
+Each FDW entry supports the following properties:
+
+- `name`: The name of the foreign data wrapper **(mandatory)**.
+- `ensure`: Indicates whether the FDW should be `present` or `absent` in the
+  database (default is `present`).
+- `handler`: The name of the handler function used by the FDW. If not
+  specified, the default handler defined by the FDW extension (if any) will be
+  used.
+- `validator`: The name of the validator function used by the FDW. If not
+  specified, the default validator defined by the FDW extension (if any) will
+  be used.
+- `owner`: The owner of the FDW **(must be a superuser)**.
+- `usage`: The list of `USAGE` permissions of the FDW, with the following fields:
+    - `name` : The name of the role to which the usage permission should be
+      granted or from which it should be revoked.
+    - `type` : The type of the usage permission. Supports `grant` and `revoke`.
+- `options`: A map of FDW-specific options to manage, where each key is the
+  name of an option. Each option supports the following fields:
+    - `value`: The string value of the option.
+    - `ensure`: Indicates whether the option should be `present` or `absent`.
+
+!!! Info
+    Both `handler` and `validator` are optional, and if not specified, the
+    default handler and validator defined by the FDW extension (if any) will be
+    used. Setting `handler` or `validator` to `"-"` will remove the handler or
+    validator from the FDW respectively. This follows the PostgreSQL convention,
+    where "-" denotes the absence of a handler or validator.
+
+!!! Warning
+    PostgreSQL restricts ownership of foreign data wrappers to **roles with
+    superuser privileges only**. Attempting to assign ownership to a non-superuser
+    (e.g., an app role) will be ignored or rejected, as PostgreSQL does not allow
+    non-superuser ownership of foreign data wrappers.
+
+The operator reconciles only the FDWs explicitly listed in `spec.fdws`. Any
+existing FDWs not declared in this list are left untouched.
+
+!!! Info
+     CloudNativePG manages FDWs using PostgreSQL's native SQL commands:
+     [`CREATE FOREIGN DATA WRAPPER`](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html),
+     [`ALTER FOREIGN DATA WRAPPER`](https://www.postgresql.org/docs/current/sql-alterforeigndatawrapper.html),
+     and [`DROP FOREIGN DATA WRAPPER`](https://www.postgresql.org/docs/current/sql-dropforeigndatawrapper.html).
+     The `ALTER` command supports option updates.
+
 ## Limitations and Caveats
 
 ### Renaming a database
diff --git a/docs/src/index.md b/docs/src/index.md
index 7b8ebfac..b2af7908 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -95,7 +95,7 @@ Additionally, the community provides images for the [PostGIS extension](postgis.
 - Declarative management of key PostgreSQL configurations, including:
     - PostgreSQL settings.
     - Roles, users, and groups.
-    - Databases, extensions, and schemas.
+    - Databases, extensions, schemas, and foreign data wrappers (FDW).
     - Tablespaces (including temporary tablespaces).
 - Flexible instance definition, supporting any number of instances (minimum 1
   primary server).
diff --git a/docs/src/operator_capability_levels.md b/docs/src/operator_capability_levels.md
index 4c4a4ad2..a73eba0d 100644
--- a/docs/src/operator_capability_levels.md
+++ b/docs/src/operator_capability_levels.md
@@ -148,7 +148,8 @@ required, as part of the bootstrap.
 
 Additional databases can be created or managed via
 [declarative database management](declarative_database_management.md) using
-the `Database` CRD, also supporting extensions and schemas.
+the `Database` CRD, also supporting extensions, schemas and foreign data
+wrappers (FDW).
 
 Although no configuration is required to run the cluster, you can customize
 both PostgreSQL runtime configuration and PostgreSQL host-based
diff --git a/internal/management/controller/database_controller.go b/internal/management/controller/database_controller.go
index 30580b90..7857506d 100644
--- a/internal/management/controller/database_controller.go
+++ b/internal/management/controller/database_controller.go
@@ -67,6 +67,14 @@ var extensionObjectManager = databaseObjectManager[apiv1.ExtensionSpec, extInfo]
 	drop:   dropDatabaseExtension,
 }
 
+// fdwObjectManager is the manager of the fdw objects
+var fdwObjectManager = databaseObjectManager[apiv1.FDWSpec, fdwInfo]{
+	get:    getDatabaseFDWInfo,
+	create: createDatabaseFDW,
+	update: updateDatabaseFDW,
+	drop:   dropDatabaseFDW,
+}
+
 // databaseReconciliationInterval is the time between the
 // database reconciliation loop failures
 const databaseReconciliationInterval = 30 * time.Second
@@ -243,6 +251,11 @@ func (r *DatabaseReconciler) reconcileDatabaseResource(ctx context.Context, obj
 			return ErrFailedDatabaseObjectReconciliation
 		}
 	}
+	for _, status := range obj.Status.FDWs {
+		if !status.Applied {
+			return ErrFailedDatabaseObjectReconciliation
+		}
+	}
 
 	return nil
 }
@@ -251,7 +264,12 @@ func (r *DatabaseReconciler) reconcileDatabaseObjects(
 	ctx context.Context,
 	obj *apiv1.Database,
 ) error {
-	if len(obj.Spec.Schemas) == 0 && len(obj.Spec.Extensions) == 0 {
+	objectCount := 0
+	objectCount += len(obj.Spec.Schemas)
+	objectCount += len(obj.Spec.Extensions)
+	objectCount += len(obj.Spec.FDWs)
+
+	if objectCount == 0 {
 		return nil
 	}
 
@@ -262,6 +280,8 @@ func (r *DatabaseReconciler) reconcileDatabaseObjects(
 
 	obj.Status.Schemas = schemaObjectManager.reconcileList(ctx, db, obj.Spec.Schemas)
 	obj.Status.Extensions = extensionObjectManager.reconcileList(ctx, db, obj.Spec.Extensions)
+	obj.Status.FDWs = fdwObjectManager.reconcileList(ctx, db, obj.Spec.FDWs)
+
 	return nil
 }
 
diff --git a/internal/management/controller/database_controller_sql.go b/internal/management/controller/database_controller_sql.go
index c2211ce8..83289800 100644
--- a/internal/management/controller/database_controller_sql.go
+++ b/internal/management/controller/database_controller_sql.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/cloudnative-pg/machinery/pkg/log"
 	"github.com/jackc/pgx/v5"
+	"github.com/lib/pq"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
 )
@@ -43,6 +44,14 @@ type schemaInfo struct {
 	Owner string `json:"owner"`
 }
 
+type fdwInfo struct {
+	Name      string                           `json:"name"`
+	Handler   string                           `json:"handler"`
+	Validator string                           `json:"validator"`
+	Owner     string                           `json:"owner"`
+	Options   map[string]apiv1.OptionSpecValue `json:"options"`
+}
+
 func detectDatabase(
 	ctx context.Context,
 	db *sql.DB,
@@ -404,3 +413,292 @@ func dropDatabaseSchema(ctx context.Context, db *sql.DB, schema apiv1.SchemaSpec
 	contextLogger.Info("dropped schema", "name", schema.Name)
 	return nil
 }
+
+const detectDatabaseFDWSQL = `
+SELECT
+ fdwname, fdwhandler::regproc::text, fdwvalidator::regproc::text, fdwoptions,
+ a.rolname AS owner
+FROM pg_foreign_data_wrapper f
+JOIN pg_authid a ON f.fdwowner = a.oid
+WHERE fdwname = $1
+`
+
+func getDatabaseFDWInfo(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) (*fdwInfo, error) {
+	contextLogger := log.FromContext(ctx)
+
+	row := db.QueryRowContext(
+		ctx, detectDatabaseFDWSQL,
+		fdw.Name)
+	if row.Err() != nil {
+		return nil, fmt.Errorf("while checking if FDW %q exists: %w", fdw.Name, row.Err())
+	}
+
+	var (
+		result     fdwInfo
+		optionsRaw pq.StringArray
+	)
+
+	if err := row.Scan(&result.Name, &result.Handler, &result.Validator, &optionsRaw, &result.Owner); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("while scanning if FDW %q exists: %w", fdw.Name, err)
+	}
+
+	// Extract options from SQL raw format(e.g. -{host=localhost,port=5432}) to type OptSpec
+	opts := make(map[string]apiv1.OptionSpecValue, len(optionsRaw))
+	for _, opt := range optionsRaw {
+		parts := strings.SplitN(opt, "=", 2)
+		if len(parts) == 2 {
+			opts[parts[0]] = apiv1.OptionSpecValue{
+				Value: parts[1],
+			}
+		} else {
+			contextLogger.Info(
+				"skipping unparsable option, expected \"keyword=value\"",
+				"optionsRaw", optionsRaw,
+				"fdwName", fdw.Name)
+		}
+	}
+	result.Options = opts
+
+	return &result, nil
+}
+
+// updateDatabaseFDWUsage updates the usage permissions for a foreign data wrapper
+// based on the provided FDW specification.
+// It supports granting or revoking usage permissions for specified users.
+func updateDatabaseFDWUsage(ctx context.Context, db *sql.DB, fdw *apiv1.FDWSpec) error {
+	contextLogger := log.FromContext(ctx)
+
+	for _, usageSpec := range fdw.Usages {
+		switch usageSpec.Type {
+		case "grant":
+			changeUsageSQL := fmt.Sprintf(
+				"GRANT USAGE ON FOREIGN DATA WRAPPER %s TO %s",
+				pgx.Identifier{fdw.Name}.Sanitize(),
+				pgx.Identifier{usageSpec.Name}.Sanitize())
+			if _, err := db.ExecContext(ctx, changeUsageSQL); err != nil {
+				return fmt.Errorf("granting usage of foreign data wrapper %w", err)
+			}
+			contextLogger.Info("granted usage of foreign data wrapper", "name", fdw.Name, "user", usageSpec.Name)
+
+		case "revoke":
+			changeUsageSQL := fmt.Sprintf(
+				"REVOKE USAGE ON FOREIGN DATA WRAPPER %s FROM %s", // #nosec G201
+				pgx.Identifier{fdw.Name}.Sanitize(),
+				pgx.Identifier{usageSpec.Name}.Sanitize())
+			if _, err := db.ExecContext(ctx, changeUsageSQL); err != nil {
+				return fmt.Errorf("revoking usage of foreign data wrapper %w", err)
+			}
+			contextLogger.Info("revoked usage of foreign data wrapper", "name", fdw.Name, "user", usageSpec.Name)
+
+		default:
+			contextLogger.Warning(
+				"unknown usage type",
+				"type", usageSpec.Type, "fdwName", fdw.Name)
+		}
+	}
+
+	return nil
+}
+
+func createDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) error {
+	contextLogger := log.FromContext(ctx)
+
+	var sqlCreateFDW strings.Builder
+	sqlCreateFDW.WriteString(fmt.Sprintf("CREATE FOREIGN DATA WRAPPER %s ", pgx.Identifier{fdw.Name}.Sanitize()))
+
+	// Create Handler
+	if len(fdw.Handler) > 0 {
+		switch fdw.Handler {
+		case "-":
+			sqlCreateFDW.WriteString("NO HANDLER ")
+		default:
+			sqlCreateFDW.WriteString(fmt.Sprintf("HANDLER %s ", pgx.Identifier{fdw.Handler}.Sanitize()))
+		}
+	}
+
+	// Create Validator
+	if len(fdw.Validator) > 0 {
+		switch fdw.Validator {
+		case "-":
+			sqlCreateFDW.WriteString("NO VALIDATOR ")
+		default:
+			sqlCreateFDW.WriteString(fmt.Sprintf("VALIDATOR %s ", pgx.Identifier{fdw.Validator}.Sanitize()))
+		}
+	}
+
+	// Extract options
+	opts := make([]string, 0, len(fdw.Options))
+	for _, optionSpec := range fdw.Options {
+		if optionSpec.Ensure == apiv1.EnsureAbsent {
+			continue
+		}
+		opts = append(opts, fmt.Sprintf("%s '%s'", pgx.Identifier{optionSpec.Name}.Sanitize(),
+			optionSpec.Value))
+	}
+	if len(opts) > 0 {
+		sqlCreateFDW.WriteString("OPTIONS (" + strings.Join(opts, ", ") + ")")
+	}
+
+	_, err := db.ExecContext(ctx, sqlCreateFDW.String())
+	if err != nil {
+		contextLogger.Error(err, "while creating foreign data wrapper", "query", sqlCreateFDW.String())
+		return err
+	}
+	contextLogger.Info("created foreign data wrapper", "name", fdw.Name)
+
+	// Update usage permissions
+	if len(fdw.Usages) > 0 {
+		if err := updateDatabaseFDWUsage(ctx, db, &fdw); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func updateFDWOptions(ctx context.Context, db *sql.DB, fdw *apiv1.FDWSpec, info *fdwInfo) error {
+	contextLogger := log.FromContext(ctx)
+
+	// Collect individual ALTER-clauses
+	var clauses []string
+	for _, desiredOptSpec := range fdw.Options {
+		curOptSpec, exists := info.Options[desiredOptSpec.Name]
+
+		switch {
+		case desiredOptSpec.Ensure == apiv1.EnsurePresent && !exists:
+			clauses = append(clauses, fmt.Sprintf("ADD %s '%s'",
+				pgx.Identifier{desiredOptSpec.Name}.Sanitize(), desiredOptSpec.Value))
+
+		case desiredOptSpec.Ensure == apiv1.EnsurePresent && exists:
+			if desiredOptSpec.Value != curOptSpec.Value {
+				clauses = append(clauses, fmt.Sprintf("SET %s '%s'",
+					pgx.Identifier{desiredOptSpec.Name}.Sanitize(), desiredOptSpec.Value))
+			}
+
+		case desiredOptSpec.Ensure == apiv1.EnsureAbsent && exists:
+			clauses = append(clauses, fmt.Sprintf("DROP %s", pgx.Identifier{desiredOptSpec.Name}.Sanitize()))
+		}
+	}
+
+	if len(clauses) == 0 {
+		return nil
+	}
+
+	// Build SQL
+	changeOptionSQL := fmt.Sprintf(
+		"ALTER FOREIGN DATA WRAPPER %s OPTIONS (%s)", pgx.Identifier{fdw.Name}.Sanitize(),
+		strings.Join(clauses, ", "),
+	)
+
+	if _, err := db.ExecContext(ctx, changeOptionSQL); err != nil {
+		return fmt.Errorf("altering options of foreign data wrapper %w", err)
+	}
+	contextLogger.Info("altered foreign data wrapper options", "name", fdw.Name, "options", fdw.Options)
+
+	return nil
+}
+
+func updateDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec, info *fdwInfo) error {
+	contextLogger := log.FromContext(ctx)
+
+	// Alter Handler
+	if len(fdw.Handler) > 0 && fdw.Handler != info.Handler {
+		switch fdw.Handler {
+		case "-":
+			changeHandlerSQL := fmt.Sprintf(
+				"ALTER FOREIGN DATA WRAPPER %s NO HANDLER",
+				pgx.Identifier{fdw.Name}.Sanitize(),
+			)
+			if _, err := db.ExecContext(ctx, changeHandlerSQL); err != nil {
+				return fmt.Errorf("removing handler of foreign data wrapper %w", err)
+			}
+			contextLogger.Info("removed foreign data wrapper handler", "name", fdw.Name)
+
+		default:
+			changeHandlerSQL := fmt.Sprintf(
+				"ALTER FOREIGN DATA WRAPPER %s HANDLER %s",
+				pgx.Identifier{fdw.Name}.Sanitize(),
+				pgx.Identifier{fdw.Handler}.Sanitize(),
+			)
+			if _, err := db.ExecContext(ctx, changeHandlerSQL); err != nil {
+				return fmt.Errorf("altering handler of foreign data wrapper %w", err)
+			}
+			contextLogger.Info("altered foreign data wrapper handler", "name", fdw.Name, "handler", fdw.Handler)
+		}
+	}
+
+	// Alter Validator
+	if len(fdw.Validator) > 0 && fdw.Validator != info.Validator {
+		switch fdw.Validator {
+		case "-":
+			changeValidatorSQL := fmt.Sprintf(
+				"ALTER FOREIGN DATA WRAPPER %s NO VALIDATOR",
+				pgx.Identifier{fdw.Name}.Sanitize(),
+			)
+
+			if _, err := db.ExecContext(ctx, changeValidatorSQL); err != nil {
+				return fmt.Errorf("removing validator of foreign data wrapper %w", err)
+			}
+
+			contextLogger.Info("removed foreign data wrapper validator", "name", fdw.Name)
+
+		default:
+			changeValidatorSQL := fmt.Sprintf(
+				"ALTER FOREIGN DATA WRAPPER %s VALIDATOR %s",
+				pgx.Identifier{fdw.Name}.Sanitize(),
+				pgx.Identifier{fdw.Validator}.Sanitize(),
+			)
+			if _, err := db.ExecContext(ctx, changeValidatorSQL); err != nil {
+				return fmt.Errorf("altering validator of foreign data wrapper %w", err)
+			}
+
+			contextLogger.Info("altered foreign data wrapper validator", "name", fdw.Name, "validator", fdw.Validator)
+		}
+	}
+
+	// Alter the owner
+	if len(fdw.Owner) > 0 && fdw.Owner != info.Owner {
+		changeOwnerSQL := fmt.Sprintf(
+			"ALTER FOREIGN DATA WRAPPER %s OWNER TO %v",
+			pgx.Identifier{fdw.Name}.Sanitize(),
+			pgx.Identifier{fdw.Owner}.Sanitize(),
+		)
+
+		if _, err := db.ExecContext(ctx, changeOwnerSQL); err != nil {
+			return fmt.Errorf("altering owner of foreign data wrapper %w", err)
+		}
+
+		contextLogger.Info("altered foreign data wrapper owner", "name", fdw.Name, "owner", fdw.Owner)
+	}
+
+	// Alter Options
+	if err := updateFDWOptions(ctx, db, &fdw, info); err != nil {
+		return err
+	}
+
+	// Update usage permissions
+	if len(fdw.Usages) > 0 {
+		if err := updateDatabaseFDWUsage(ctx, db, &fdw); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func dropDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) error {
+	contextLogger := log.FromContext(ctx)
+	query := fmt.Sprintf("DROP FOREIGN DATA WRAPPER IF EXISTS %s", pgx.Identifier{fdw.Name}.Sanitize())
+	_, err := db.ExecContext(
+		ctx,
+		query)
+	if err != nil {
+		contextLogger.Error(err, "while dropping foreign data wrapper", "query", query)
+		return err
+	}
+	contextLogger.Info("dropped foreign data wrapper", "name", fdw.Name)
+	return nil
+}
diff --git a/internal/webhook/v1/database_webhook.go b/internal/webhook/v1/database_webhook.go
index 34249556..f4250dc6 100644
--- a/internal/webhook/v1/database_webhook.go
+++ b/internal/webhook/v1/database_webhook.go
@@ -154,6 +154,7 @@ func (v *DatabaseCustomValidator) validate(d *apiv1.Database) (allErrs field.Err
 	validations := []validationFunc{
 		v.validateExtensions,
 		v.validateSchemas,
+		v.validateFDWs,
 	}
 
 	for _, validate := range validations {
@@ -212,3 +213,61 @@ func (v *DatabaseCustomValidator) validateSchemas(d *apiv1.Database) field.Error
 
 	return result
 }
+
+// validateFDWs validates the database Foreign Data Wrappers
+// FDWs must be unique in .spec.fdws
+func (v *DatabaseCustomValidator) validateFDWs(d *apiv1.Database) field.ErrorList {
+	var result field.ErrorList
+
+	fdwNames := stringset.New()
+	for i, fdw := range d.Spec.FDWs {
+		name := fdw.Name
+		if fdwNames.Has(name) {
+			result = append(
+				result,
+				field.Duplicate(
+					field.NewPath("spec", "fdws").Index(i).Child("name"),
+					name,
+				),
+			)
+		}
+
+		// Validate the options of the FDW
+		optionNames := stringset.New()
+		for k, option := range fdw.Options {
+			optionName := option.Name
+			if optionNames.Has(optionName) {
+				result = append(
+					result,
+					field.Duplicate(
+						field.NewPath("spec", "fdws").Index(i).Child("options").Index(k).Child("name"),
+						optionName,
+					),
+				)
+			}
+
+			optionNames.Put(optionName)
+		}
+
+		// Validate the usage of the FDW
+		usageNames := stringset.New()
+		for j, usage := range fdw.Usages {
+			usageName := usage.Name
+			if usageNames.Has(usageName) {
+				result = append(
+					result,
+					field.Duplicate(
+						field.NewPath("spec", "fdws").Index(i).Child("usages").Index(j).Child("name"),
+						usageName,
+					),
+				)
+			}
+
+			usageNames.Put(usageName)
+		}
+
+		fdwNames.Put(name)
+	}
+
+	return result
+}



Test patch:
diff --git a/internal/management/controller/database_controller_sql_test.go b/internal/management/controller/database_controller_sql_test.go
index 3949d764..f520039f 100644
--- a/internal/management/controller/database_controller_sql_test.go
+++ b/internal/management/controller/database_controller_sql_test.go
@@ -516,3 +516,315 @@ var _ = Describe("Managed schema SQL", func() {
 		})
 	})
 })
+
+var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
+	var (
+		dbMock sqlmock.Sqlmock
+		db     *sql.DB
+		fdw    apiv1.FDWSpec
+		err    error
+
+		testError error
+	)
+
+	BeforeEach(func() {
+		db, dbMock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		Expect(err).ToNot(HaveOccurred())
+
+		fdw = apiv1.FDWSpec{
+			DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+				Name:   "testfdw",
+				Ensure: "present",
+			},
+			Handler:   "testhandler",
+			Validator: "testvalidator",
+			Options: []apiv1.OptionSpec{
+				{
+					Name: "testoption",
+					OptionSpecValue: apiv1.OptionSpecValue{
+						Value: "testvalue",
+					},
+				},
+			},
+			Owner: "owner",
+		}
+
+		testError = fmt.Errorf("test error")
+	})
+
+	AfterEach(func() {
+		Expect(dbMock.ExpectationsWereMet()).To(Succeed())
+	})
+
+	Context("getDatabaseFDWInfo", func() {
+		It("returns info when the fdw exits", func(ctx SpecContext) {
+			dbMock.
+				ExpectQuery(detectDatabaseFDWSQL).
+				WithArgs(fdw.Name).
+				WillReturnRows(
+					sqlmock.NewRows([]string{"fdwname", "fdwhandler", "fdwvalidator", "options", "fdwowner"}).
+						AddRow("testfdw", "testhandler", "testvalidator", nil, "testowner"),
+				)
+			fdwInfo, err := getDatabaseFDWInfo(ctx, db, fdw)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(fdwInfo).ToNot(BeNil())
+			Expect(fdwInfo.Name).To(Equal("testfdw"))
+			Expect(fdwInfo.Handler).To(Equal("testhandler"))
+			Expect(fdwInfo.Validator).To(Equal("testvalidator"))
+			Expect(fdwInfo.Owner).To(Equal("testowner"))
+		})
+
+		It("returns nil info when the fdw does not exist", func(ctx SpecContext) {
+			dbMock.
+				ExpectQuery(detectDatabaseFDWSQL).
+				WithArgs(fdw.Name).
+				WillReturnRows(
+					sqlmock.NewRows([]string{"fdwname", "fdwhandler", "fdwvalidator", "options", "fdwowner"}),
+				)
+			fdwInfo, err := getDatabaseFDWInfo(ctx, db, fdw)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(fdwInfo).To(BeNil())
+		})
+	})
+
+	Context("createDatabaseFDW", func() {
+		createFDWSQL := "CREATE FOREIGN DATA WRAPPER \"testfdw\" HANDLER \"testhandler\" " +
+			"VALIDATOR \"testvalidator\" OPTIONS (\"testoption\" 'testvalue')"
+
+		It("returns success when the fdw has been created", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(createFDWSQL).
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			Expect(createDatabaseFDW(ctx, db, fdw)).Error().NotTo(HaveOccurred())
+		})
+
+		It("fails when the fdw could not be created", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(createFDWSQL).
+				WillReturnError(testError)
+			Expect(createDatabaseFDW(ctx, db, fdw)).Error().To(Equal(testError))
+		})
+
+		It("success with NO HANDLER and NO VALIDATOR", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("CREATE FOREIGN DATA WRAPPER \"testfdw\" NO HANDLER NO VALIDATOR").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(createDatabaseFDW(ctx, db, apiv1.FDWSpec{
+				DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+					Name:   "testfdw",
+					Ensure: "present",
+				},
+				Handler:   "-",
+				Validator: "-",
+				Owner:     "owner",
+			})).Error().NotTo(HaveOccurred())
+		})
+	})
+
+	Context("updateDatabaseFDW", func() {
+		It("does nothing when the fdw has been correctly reconciled", func(ctx SpecContext) {
+			Expect(updateDatabaseFDW(ctx, db, fdw, &fdwInfo{
+				Name:      fdw.Name,
+				Handler:   fdw.Handler,
+				Validator: fdw.Validator,
+				Owner:     fdw.Owner,
+			})).Error().NotTo(HaveOccurred())
+		})
+
+		It("updates the fdw handler", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" HANDLER \"testhandler\"").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: "oldhandler", Validator: fdw.Validator, Owner: fdw.Owner})).
+				Error().NotTo(HaveOccurred())
+		})
+
+		It("handles removal of handler when not specified", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" NO HANDLER").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			fdw.Handler = "-"
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: "oldhandler", Validator: fdw.Validator, Owner: fdw.Owner})).
+				Error().NotTo(HaveOccurred())
+		})
+
+		It("fail when setting the handler failed", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" HANDLER \"testhandler\"").
+				WillReturnError(testError)
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: "oldhandler", Validator: fdw.Validator, Owner: fdw.Owner})).
+				Error().To(MatchError(testError))
+		})
+
+		It("updates the fdw validator", func(ctx SpecContext) {
+			dbMock.ExpectExec(
+				"ALTER FOREIGN DATA WRAPPER \"testfdw\" VALIDATOR \"testvalidator\"").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: fdw.Handler, Validator: "oldvalidator", Owner: fdw.Owner})).
+				Error().NotTo(HaveOccurred())
+		})
+
+		It("handles removal of validator when not specified", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" NO VALIDATOR").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			fdw.Validator = "-"
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: fdw.Handler, Validator: "oldvalidator", Owner: fdw.Owner})).
+				Error().NotTo(HaveOccurred())
+		})
+
+		It("fail when setting the validator failed", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" VALIDATOR \"testvalidator\"").
+				WillReturnError(testError)
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: fdw.Handler, Validator: "oldvalidator", Owner: fdw.Owner})).
+				Error().To(MatchError(testError))
+		})
+
+		It("add new fdw options", func(ctx SpecContext) {
+			fdw.Options = []apiv1.OptionSpec{
+				{
+					Name: "add_option",
+					OptionSpecValue: apiv1.OptionSpecValue{
+						Value:  "value",
+						Ensure: apiv1.EnsurePresent,
+					},
+				},
+			}
+			info := &fdwInfo{
+				Name:      fdw.Name,
+				Handler:   fdw.Handler,
+				Validator: fdw.Validator,
+				Options: map[string]apiv1.OptionSpecValue{
+					"modify_option": {Value: "old_value"},
+					"remove_option": {Value: "value"},
+				},
+				Owner: fdw.Owner,
+			}
+
+			expectedSQL := "ALTER FOREIGN DATA WRAPPER \"testfdw\" OPTIONS (ADD \"add_option\" 'value')"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw, info)).Error().NotTo(HaveOccurred())
+		})
+
+		It("modify the fdw options", func(ctx SpecContext) {
+			fdw.Options = []apiv1.OptionSpec{
+				{
+					Name: "modify_option",
+					OptionSpecValue: apiv1.OptionSpecValue{
+						Value:  "new_value",
+						Ensure: apiv1.EnsurePresent,
+					},
+				},
+			}
+			info := &fdwInfo{
+				Name:      fdw.Name,
+				Handler:   fdw.Handler,
+				Validator: fdw.Validator,
+				Options: map[string]apiv1.OptionSpecValue{
+					"modify_option": {Value: "old_value"},
+					"remove_option": {Value: "value"},
+				},
+				Owner: fdw.Owner,
+			}
+
+			expectedSQL := "ALTER FOREIGN DATA WRAPPER \"testfdw\" OPTIONS (SET \"modify_option\" 'new_value')"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw, info)).Error().NotTo(HaveOccurred())
+		})
+
+		It("remove new fdw options", func(ctx SpecContext) {
+			fdw.Options = []apiv1.OptionSpec{
+				{
+					Name: "remove_option",
+					OptionSpecValue: apiv1.OptionSpecValue{
+						Value:  "value",
+						Ensure: apiv1.EnsureAbsent,
+					},
+				},
+			}
+			info := &fdwInfo{
+				Name:      fdw.Name,
+				Handler:   fdw.Handler,
+				Validator: fdw.Validator,
+				Options: map[string]apiv1.OptionSpecValue{
+					"modify_option": {Value: "old_value"},
+					"remove_option": {Value: "value"},
+				},
+				Owner: fdw.Owner,
+			}
+
+			expectedSQL := "ALTER FOREIGN DATA WRAPPER \"testfdw\" OPTIONS (DROP \"remove_option\")"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw, info)).Error().NotTo(HaveOccurred())
+		})
+
+		It("updates the fdw owner", func(ctx SpecContext) {
+			dbMock.ExpectExec(
+				"ALTER FOREIGN DATA WRAPPER \"testfdw\" OWNER TO \"owner\"").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: fdw.Handler, Validator: fdw.Validator, Owner: "oldowner"})).
+				Error().NotTo(HaveOccurred())
+		})
+
+		It("fail when setting the owner failed", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec("ALTER FOREIGN DATA WRAPPER \"testfdw\" OWNER TO \"owner\"").
+				WillReturnError(testError)
+
+			Expect(updateDatabaseFDW(ctx, db, fdw,
+				&fdwInfo{Name: fdw.Name, Handler: fdw.Handler, Validator: fdw.Validator, Owner: "old"})).
+				Error().To(MatchError(testError))
+		})
+
+		It("updates the usages permissions of the fdw", func(ctx SpecContext) {
+			dbMock.ExpectExec(
+				"GRANT USAGE ON FOREIGN DATA WRAPPER \"testfdw\" TO \"owner\"").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			fdw.Usages = []apiv1.UsageSpec{
+				{
+					Name: "owner",
+					Type: "grant",
+				},
+			}
+			Expect(updateDatabaseFDWUsage(ctx, db, &fdw)).Error().NotTo(HaveOccurred())
+		})
+	})
+
+	Context("dropDatabaseFDW", func() {
+		dropFDWSQL := "DROP FOREIGN DATA WRAPPER IF EXISTS \"testfdw\""
+
+		It("returns success when the foreign data wrapper has been dropped", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(dropFDWSQL).
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			Expect(dropDatabaseFDW(ctx, db, fdw)).Error().NotTo(HaveOccurred())
+		})
+
+		It("returns an error when the DROP statement failed", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(dropFDWSQL).
+				WillReturnError(testError)
+
+			Expect(dropDatabaseFDW(ctx, db, fdw)).Error().To(Equal(testError))
+		})
+	})
+})
diff --git a/internal/webhook/v1/database_webhook_test.go b/internal/webhook/v1/database_webhook_test.go
index d0259ab1..cebb1d24 100644
--- a/internal/webhook/v1/database_webhook_test.go
+++ b/internal/webhook/v1/database_webhook_test.go
@@ -46,6 +46,14 @@ var _ = Describe("Database validation", func() {
 		}
 	}
 
+	createFDWSpec := func(name string) apiv1.FDWSpec {
+		return apiv1.FDWSpec{
+			DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+				Name:   name,
+				Ensure: apiv1.EnsurePresent,
+			},
+		}
+	}
 	BeforeEach(func() {
 		v = &DatabaseCustomValidator{}
 	})
@@ -104,5 +112,111 @@ var _ = Describe("Database validation", func() {
 			},
 			1,
 		),
+
+		Entry(
+			"doesn't complain with distinct FDWs and usage names",
+			&apiv1.Database{
+				Spec: apiv1.DatabaseSpec{
+					FDWs: []apiv1.FDWSpec{
+						{
+							DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+								Name:   "fdw1",
+								Ensure: apiv1.EnsurePresent,
+							},
+							Usages: []apiv1.UsageSpec{
+								{Name: "usage1"},
+								{Name: "usage2"},
+							},
+							Options: []apiv1.OptionSpec{
+								{Name: "option1"},
+								{Name: "option2"},
+							},
+						},
+						{
+							DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+								Name:   "fdw2",
+								Ensure: apiv1.EnsurePresent,
+							},
+							Usages: []apiv1.UsageSpec{
+								{Name: "usage3"},
+								{Name: "usage4"},
+							},
+							Options: []apiv1.OptionSpec{
+								{Name: "option3"},
+								{Name: "option4"},
+							},
+						},
+					},
+				},
+			},
+			0,
+		),
+
+		Entry(
+			"complain if there are duplicate FDWs",
+			&apiv1.Database{
+				Spec: apiv1.DatabaseSpec{
+					FDWs: []apiv1.FDWSpec{
+						createFDWSpec("postgres_fdw"),
+						createFDWSpec("mysql_fdw"),
+						createFDWSpec("postgres_fdw"),
+					},
+				},
+			},
+			1,
+		),
+
+		Entry(
+			"complain if there are duplicate usage names within an FDW",
+			&apiv1.Database{
+				Spec: apiv1.DatabaseSpec{
+					FDWs: []apiv1.FDWSpec{
+						{
+							DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+								Name:   "postgre_fdw",
+								Ensure: apiv1.EnsurePresent,
+							},
+							Usages: []apiv1.UsageSpec{
+								{Name: "usage1"},
+								{Name: "usage2"},
+								{Name: "usage1"},
+							},
+						},
+					},
+				},
+			},
+			1,
+		),
+
+		Entry(
+			"complains for duplicate FDW and duplicate usage names",
+			&apiv1.Database{
+				Spec: apiv1.DatabaseSpec{
+					FDWs: []apiv1.FDWSpec{
+						{
+							DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+								Name:   "duplicate_fdw",
+								Ensure: apiv1.EnsurePresent,
+							},
+							Usages: []apiv1.UsageSpec{
+								{Name: "dup_usage"},
+								{Name: "dup_usage"},
+							},
+							Options: []apiv1.OptionSpec{
+								{Name: "dup_option"},
+								{Name: "dup_option"},
+							},
+						},
+						{
+							DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+								Name:   "duplicate_fdw",
+								Ensure: apiv1.EnsurePresent,
+							},
+						},
+					},
+				},
+			},
+			3,
+		),
 	)
 })
diff --git a/tests/e2e/asserts_test.go b/tests/e2e/asserts_test.go
index 85bbe3d7..9ba006e1 100644
--- a/tests/e2e/asserts_test.go
+++ b/tests/e2e/asserts_test.go
@@ -553,6 +553,10 @@ func schemaExistsQuery(namespaceName string) string {
 	return fmt.Sprintf("SELECT EXISTS(SELECT FROM pg_catalog.pg_namespace WHERE nspname='%v')", namespaceName)
 }
 
+func fdwExistsQuery(fdwName string) string {
+	return fmt.Sprintf("SELECT EXISTS(SELECT FROM pg_catalog.pg_foreign_data_wrapper WHERE fdwname='%v')", fdwName)
+}
+
 // AssertDataExpectedCount verifies that an expected amount of rows exists on the table
 func AssertDataExpectedCount(
 	env *environment.TestingEnvironment,
diff --git a/tests/e2e/declarative_database_management_test.go b/tests/e2e/declarative_database_management_test.go
index d896b798..9ccebdf6 100644
--- a/tests/e2e/declarative_database_management_test.go
+++ b/tests/e2e/declarative_database_management_test.go
@@ -154,6 +154,16 @@ var _ = Describe("Declarative database management", Label(tests.LabelSmoke, test
 				}
 			})
 
+			By("verifying the fdw presence in the target database", func() {
+				primaryPodInfo, err := clusterutils.GetPrimary(env.Ctx, env.Client, namespace, clusterName)
+				Expect(err).ToNot(HaveOccurred())
+
+				for _, fdwSpec := range database.Spec.FDWs {
+					Eventually(QueryMatchExpectationPredicate(primaryPodInfo, exec.DatabaseName(database.Spec.Name),
+						fdwExistsQuery(fdwSpec.Name), boolPGOutput(true)), 30).Should(Succeed())
+				}
+			})
+
 			By("removing the Database object", func() {
 				Expect(objects.Delete(env.Ctx, env.Client, &database)).To(Succeed())
 			})
diff --git a/tests/e2e/fixtures/declarative_databases/database-with-delete-reclaim-policy.yaml.template b/tests/e2e/fixtures/declarative_databases/database-with-delete-reclaim-policy.yaml.template
index 40f7cfe7..ec59c21a 100644
--- a/tests/e2e/fixtures/declarative_databases/database-with-delete-reclaim-policy.yaml.template
+++ b/tests/e2e/fixtures/declarative_databases/database-with-delete-reclaim-policy.yaml.template
@@ -14,6 +14,20 @@ spec:
   extensions:
   - name: bloom
     ensure: present
+  - name: postgres_fdw
+    ensure: present
   schemas:
   - name: test_schema
     ensure: present
+  fdws:
+    - name: postgres_fdw
+      ensure: present
+      usage:
+      - name: app
+        type: grant
+    - name: mywrapper
+      options:
+        - name: debug
+          value: 'true'
+          ensure: present
+      ensure: present
diff --git a/tests/e2e/fixtures/declarative_databases/database.yaml.template b/tests/e2e/fixtures/declarative_databases/database.yaml.template
index a9c43b0d..1d67109a 100644
--- a/tests/e2e/fixtures/declarative_databases/database.yaml.template
+++ b/tests/e2e/fixtures/declarative_databases/database.yaml.template
@@ -14,6 +14,20 @@ spec:
   extensions:
   - name: bloom
     ensure: present
+  - name: postgres_fdw
+    ensure: present
   schemas:
   - name: test_schema
     ensure: present
+  fdws:
+    - name: postgres_fdw
+      ensure: present
+      usage:
+      - name: app
+        type: grant
+    - name: mywrapper
+      options:
+        - name: debug
+          value: 'true'
+          ensure: present
+      ensure: present