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 ddfd24c7..4a3eb7ec 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -463,6 +463,7 @@ SecurityProfiles
 Seealso
 SelectorType
 ServerCASecret
+ServerSpec
 ServerTLSSecret
 ServiceAccount
 ServiceAccount's
@@ -531,6 +532,7 @@ Uncomment
 Unrealizable
 UpdateStrategy
 UsageSpec
+UsageSpecType
 VLDB
 VLDBs
 VM
diff --git a/api/v1/database_types.go b/api/v1/database_types.go
index 9baee51c..806db86a 100644
--- a/api/v1/database_types.go
+++ b/api/v1/database_types.go
@@ -38,6 +38,20 @@ const (
 	DatabaseReclaimRetain DatabaseReclaimPolicy = "retain"
 )
 
+// UsageSpecType describes the type of usage specified in the `usage` field of the
+// `Database` object.
+// +enum
+type UsageSpecType string
+
+const (
+	// GrantUsageSpecType indicates a grant usage permission.
+	// The default usage permission is grant.
+	GrantUsageSpecType UsageSpecType = "grant"
+
+	// RevokeUsageSpecType indicates a revoke usage permission.
+	RevokeUsageSpecType UsageSpecType = "revoke"
+)
+
 // DatabaseSpec is the specification of a Postgresql Database, built around the
 // `CREATE DATABASE`, `ALTER DATABASE`, and `DROP DATABASE` SQL commands of
 // PostgreSQL.
@@ -177,18 +191,22 @@ type DatabaseSpec struct {
 	// The list of foreign data wrappers to be managed in the database
 	// +optional
 	FDWs []FDWSpec `json:"fdws,omitempty"`
+
+	// The list of foreign servers to be managed in the database
+	// +optional
+	Servers []ServerSpec `json:"servers,omitempty"`
 }
 
 // DatabaseObjectSpec contains the fields which are common to every
 // database object
 type DatabaseObjectSpec struct {
-	// Name of the extension/schema
+	// Name of the object (extension, schema, FDW, server)
 	Name string `json:"name"`
 
-	// 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.
+	// Specifies whether an object (e.g schema) should be present or absent
+	// in the database. If set to `present`, the object will be created if
+	// it does not exist. If set to `absent`, the extension/schema will be
+	// removed if it exists.
 	// +kubebuilder:default:="present"
 	// +kubebuilder:validation:Enum=present;absent
 	// +optional
@@ -246,8 +264,7 @@ type FDWSpec struct {
 	// +optional
 	Owner string `json:"owner,omitempty"`
 
-	// Options specifies the configuration options for the FDW
-	// (key is the option name, value is the option value).
+	// Options specifies the configuration options for the FDW.
 	// +optional
 	Options []OptionSpec `json:"options,omitempty"`
 
@@ -256,17 +273,30 @@ type FDWSpec struct {
 	Usages []UsageSpec `json:"usage,omitempty"`
 }
 
+// ServerSpec configures a server of a foreign data wrapper
+type ServerSpec struct {
+	// Common fields
+	DatabaseObjectSpec `json:",inline"`
+
+	// The name of the Foreign Data Wrapper (FDW)
+	// +kubebuilder:validation:XValidation:rule="self != ''",message="fdw is required"
+	FdwName string `json:"fdw"`
+
+	// Options specifies the configuration options for the server
+	// (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 server 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"`
 
@@ -283,13 +313,14 @@ type OptionSpecValue struct {
 // UsageSpec configures a usage for a foreign data wrapper
 type UsageSpec struct {
 	// Name of the usage
+	// +kubebuilder:validation:XValidation:rule="self != ''",message="name is required"
 	Name string `json:"name"`
 
 	// The type of usage
 	// +kubebuilder:default:="grant"
 	// +kubebuilder:validation:Enum=grant;revoke
 	// +optional
-	Type string `json:"type,omitempty"`
+	Type UsageSpecType `json:"type,omitempty"`
 }
 
 // DatabaseStatus defines the observed state of Database
@@ -318,6 +349,10 @@ type DatabaseStatus struct {
 	// FDWs is the status of the managed FDWs
 	// +optional
 	FDWs []DatabaseObjectStatus `json:"fdws,omitempty"`
+
+	// Servers is the status of the managed servers
+	// +optional
+	Servers []DatabaseObjectStatus `json:"servers,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 65c47bb8..e712ac32 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -1173,6 +1173,13 @@ func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.Servers != nil {
+		in, out := &in.Servers, &out.Servers
+		*out = make([]ServerSpec, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseSpec.
@@ -1208,6 +1215,11 @@ func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) {
 		*out = make([]DatabaseObjectStatus, len(*in))
 		copy(*out, *in)
 	}
+	if in.Servers != nil {
+		in, out := &in.Servers, &out.Servers
+		*out = make([]DatabaseObjectStatus, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseStatus.
@@ -2023,7 +2035,6 @@ func (in *OnlineConfiguration) DeepCopy() *OnlineConfiguration {
 // 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.
@@ -2036,21 +2047,6 @@ func (in *OptionSpec) DeepCopy() *OptionSpec {
 	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
@@ -3081,6 +3077,32 @@ func (in *SecretsResourceVersion) DeepCopy() *SecretsResourceVersion {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerSpec) DeepCopyInto(out *ServerSpec) {
+	*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 ServerSpec.
+func (in *ServerSpec) DeepCopy() *ServerSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ServiceAccountTemplate) DeepCopyInto(out *ServiceAccountTemplate) {
 	*out = *in
diff --git a/config/crd/bases/postgresql.cnpg.io_databases.yaml b/config/crd/bases/postgresql.cnpg.io_databases.yaml
index 81c50608..544a04fa 100644
--- a/config/crd/bases/postgresql.cnpg.io_databases.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_databases.yaml
@@ -132,16 +132,16 @@ spec:
                     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.
+                        Specifies whether an object (e.g schema) should be present or absent
+                        in the database. If set to `present`, the object 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
                     name:
-                      description: Name of the extension/schema
+                      description: Name of the object (extension, schema, FDW, server)
                       type: string
                     schema:
                       description: |-
@@ -170,10 +170,10 @@ spec:
                     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.
+                        Specifies whether an object (e.g schema) should be present or absent
+                        in the database. If set to `present`, the object will be created if
+                        it does not exist. If set to `absent`, the extension/schema will be
+                        removed if it exists.
                       enum:
                       - present
                       - absent
@@ -185,12 +185,11 @@ spec:
                         the default handler is registered when the FDW extension is created.
                       type: string
                     name:
-                      description: Name of the extension/schema
+                      description: Name of the object (extension, schema, FDW, server)
                       type: string
                     options:
-                      description: |-
-                        Options specifies the configuration options for the FDW
-                        (key is the option name, value is the option value).
+                      description: Options specifies the configuration options for
+                        the FDW.
                       items:
                         description: OptionSpec holds the name, value and the ensure
                           field for an option
@@ -232,6 +231,9 @@ spec:
                           name:
                             description: Name of the usage
                             type: string
+                            x-kubernetes-validations:
+                            - message: name is required
+                              rule: self != ''
                           type:
                             default: grant
                             description: The type of usage
@@ -340,16 +342,16 @@ spec:
                     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.
+                        Specifies whether an object (e.g schema) should be present or absent
+                        in the database. If set to `present`, the object 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
                     name:
-                      description: Name of the extension/schema
+                      description: Name of the object (extension, schema, FDW, server)
                       type: string
                     owner:
                       description: |-
@@ -361,6 +363,90 @@ spec:
                   - name
                   type: object
                 type: array
+              servers:
+                description: The list of foreign servers to be managed in the database
+                items:
+                  description: ServerSpec configures a server of a foreign data wrapper
+                  properties:
+                    ensure:
+                      default: present
+                      description: |-
+                        Specifies whether an object (e.g schema) should be present or absent
+                        in the database. If set to `present`, the object 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
+                    fdw:
+                      description: The name of the Foreign Data Wrapper (FDW)
+                      type: string
+                      x-kubernetes-validations:
+                      - message: fdw is required
+                        rule: self != ''
+                    name:
+                      description: Name of the object (extension, schema, FDW, server)
+                      type: string
+                    options:
+                      description: |-
+                        Options specifies the configuration options for the server
+                        (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
+                    usage:
+                      description: List of roles for which `USAGE` privileges on the
+                        server are granted or revoked.
+                      items:
+                        description: UsageSpec configures a usage for a foreign data
+                          wrapper
+                        properties:
+                          name:
+                            description: Name of the usage
+                            type: string
+                            x-kubernetes-validations:
+                            - message: name is required
+                              rule: self != ''
+                          type:
+                            default: grant
+                            description: The type of usage
+                            enum:
+                            - grant
+                            - revoke
+                            type: string
+                        required:
+                        - name
+                        type: object
+                      type: array
+                  required:
+                  - fdw
+                  - name
+                  type: object
+                type: array
               tablespace:
                 description: |-
                   Maps to the `TABLESPACE` parameter of `CREATE DATABASE`.
@@ -475,6 +561,28 @@ spec:
                   - name
                   type: object
                 type: array
+              servers:
+                description: Servers is the status of the managed servers
+                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
             type: object
         required:
         - metadata
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index 77e9b4be..731f8aaf 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -2438,6 +2438,8 @@ PostgreSQL cluster from an existing storage</p>
 
 - [SchemaSpec](#postgresql-cnpg-io-v1-SchemaSpec)
 
+- [ServerSpec](#postgresql-cnpg-io-v1-ServerSpec)
+
 
 <p>DatabaseObjectSpec contains the fields which are common to every
 database object</p>
@@ -2450,17 +2452,17 @@ database object</p>
 <i>string</i>
 </td>
 <td>
-   <p>Name of the extension/schema</p>
+   <p>Name of the object (extension, schema, FDW, server)</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 extension/schema should be present or absent in
-the database. If set to <code>present</code>, the extension/schema will be
-created if it does not exist. If set to <code>absent</code>, the
-extension/schema will be removed if it exists.</p>
+   <p>Specifies whether an object (e.g schema) should be present or absent
+in the database. If set to <code>present</code>, the object will be created if
+it does not exist. If set to <code>absent</code>, the extension/schema will be
+removed if it exists.</p>
 </td>
 </tr>
 </tbody>
@@ -2741,6 +2743,13 @@ tablespace used for objects created in this database.</p>
    <p>The list of foreign data wrappers to be managed in the database</p>
 </td>
 </tr>
+<tr><td><code>servers</code><br/>
+<a href="#postgresql-cnpg-io-v1-ServerSpec"><i>[]ServerSpec</i></a>
+</td>
+<td>
+   <p>The list of foreign servers to be managed in the database</p>
+</td>
+</tr>
 </tbody>
 </table>
 
@@ -2801,6 +2810,13 @@ desired state that was synchronized</p>
    <p>FDWs is the status of the managed FDWs</p>
 </td>
 </tr>
+<tr><td><code>servers</code><br/>
+<a href="#postgresql-cnpg-io-v1-DatabaseObjectStatus"><i>[]DatabaseObjectStatus</i></a>
+</td>
+<td>
+   <p>Servers is the status of the managed servers</p>
+</td>
+</tr>
 </tbody>
 </table>
 
@@ -2843,7 +2859,7 @@ desired state that was synchronized</p>
 
 - [DatabaseSpec](#postgresql-cnpg-io-v1-DatabaseSpec)
 
-- [OptionSpecValue](#postgresql-cnpg-io-v1-OptionSpecValue)
+- [OptionSpec](#postgresql-cnpg-io-v1-OptionSpec)
 
 - [RoleConfiguration](#postgresql-cnpg-io-v1-RoleConfiguration)
 
@@ -3120,8 +3136,7 @@ The role must have superuser privileges in the target database.</p>
 <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>
+   <p>Options specifies the configuration options for the FDW.</p>
 </td>
 </tr>
 <tr><td><code>usage</code><br/>
@@ -4030,6 +4045,8 @@ possible. <code>false</code> by default.</p>
 
 - [FDWSpec](#postgresql-cnpg-io-v1-FDWSpec)
 
+- [ServerSpec](#postgresql-cnpg-io-v1-ServerSpec)
+
 
 <p>OptionSpec holds the name, value and the ensure field for an option</p>
 
@@ -4044,30 +4061,6 @@ possible. <code>false</code> by default.</p>
    <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>
@@ -5917,6 +5910,52 @@ Map keys are the secret names, map values are the versions</p>
 </tbody>
 </table>
 
+## ServerSpec     {#postgresql-cnpg-io-v1-ServerSpec}
+
+
+**Appears in:**
+
+- [DatabaseSpec](#postgresql-cnpg-io-v1-DatabaseSpec)
+
+
+<p>ServerSpec configures a server of a foreign data wrapper</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>fdw</code> <B>[Required]</B><br/>
+<i>string</i>
+</td>
+<td>
+   <p>The name of the Foreign Data Wrapper (FDW)</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 server
+(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 server are granted or revoked.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## ServiceAccountTemplate     {#postgresql-cnpg-io-v1-ServiceAccountTemplate}
 
 
@@ -6565,6 +6604,8 @@ in synchronous replica election in case of failures</p>
 
 - [FDWSpec](#postgresql-cnpg-io-v1-FDWSpec)
 
+- [ServerSpec](#postgresql-cnpg-io-v1-ServerSpec)
+
 
 <p>UsageSpec configures a usage for a foreign data wrapper</p>
 
@@ -6580,7 +6621,7 @@ in synchronous replica election in case of failures</p>
 </td>
 </tr>
 <tr><td><code>type</code><br/>
-<i>string</i>
+<a href="#postgresql-cnpg-io-v1-UsageSpecType"><i>UsageSpecType</i></a>
 </td>
 <td>
    <p>The type of usage</p>
@@ -6589,6 +6630,21 @@ in synchronous replica election in case of failures</p>
 </tbody>
 </table>
 
+## UsageSpecType     {#postgresql-cnpg-io-v1-UsageSpecType}
+
+(Alias of `string`)
+
+**Appears in:**
+
+- [UsageSpec](#postgresql-cnpg-io-v1-UsageSpec)
+
+
+<p>UsageSpecType describes the type of usage specified in the <code>usage</code> field of the
+<code>Database</code> object.</p>
+
+
+
+
 ## VolumeSnapshotConfiguration     {#postgresql-cnpg-io-v1-VolumeSnapshotConfiguration}
 
 
diff --git a/docs/src/declarative_database_management.md b/docs/src/declarative_database_management.md
index b938a65d..54ee9ef1 100644
--- a/docs/src/declarative_database_management.md
+++ b/docs/src/declarative_database_management.md
@@ -40,8 +40,8 @@ spec:
   cluster:
     name: cluster-example
   extensions:
-  - name: bloom
-    ensure: present
+    - name: bloom
+      ensure: present
 ```
 
 When applied, this manifest creates a `Database` object called
@@ -181,8 +181,8 @@ with a list of extension specifications, as shown in the following example:
 # ...
 spec:
   extensions:
-  - name: bloom
-    ensure: present
+    - name: bloom
+      ensure: present
 # ...
 ```
 
@@ -236,8 +236,8 @@ with a list of schema specifications, as shown in the following example:
 # ...
 spec:
   schemas:
-  - name: app
-    owner: app
+    - name: app
+      owner: app
 # ...
 ```
 
@@ -256,7 +256,7 @@ 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
+## Managing Foreign Data Wrappers (FDWs) in a Database
 
 !!! Info
     Foreign Data Wrappers (FDWs) are database-scoped objects that typically
@@ -275,10 +275,10 @@ specifications, as shown in the following example:
 # ...
 spec:
   fdws:
-  - name: postgres_fdw
-    usage:
-      - name: app
-      - type: grant
+    - name: postgres_fdw
+      usage:
+        - name: app
+          type: grant
 # ...
 ```
 
@@ -296,7 +296,7 @@ Each FDW entry supports the following properties:
 - `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.
+      granted or from which it should be revoked **(mandatory)**.
     - `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:
@@ -314,7 +314,8 @@ Each FDW entry supports the following properties:
     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.
+    non-superuser ownership of foreign data wrappers. By default, they are
+    owned by the `postgres` user.
 
 The operator reconciles only the FDWs explicitly listed in `spec.fdws`. Any
 existing FDWs not declared in this list are left untouched.
@@ -326,6 +327,75 @@ existing FDWs not declared in this list are left untouched.
      and [`DROP FOREIGN DATA WRAPPER`](https://www.postgresql.org/docs/current/sql-dropforeigndatawrapper.html).
      The `ALTER` command supports option updates.
 
+### Managing Foreign Servers in a Database
+
+CloudNativePG provides seamless, automated management of PostgreSQL foreign
+servers in a target database using declarative configuration.
+
+A **foreign server** encapsulates the connection details that a foreign data
+wrapper (FDW) uses to access an external data source. For user-specific
+connection details, you can define [user mappings](https://www.postgresql.org/docs/current/sql-createusermapping.html).
+
+!!! Important
+    CloudNativePG does not currently support declarative configuration of user mappings.
+    However, once an FDW and its foreign server are defined, you can grant
+    usage privileges to a standard database role. This allows you to manage user
+    mappings as part of your SQL schema, without requiring superuser privileges.
+
+To enable this feature, declare the `spec.servers` field in a `Database`
+resource with a list of foreign server specifications, for example:
+
+```yaml
+# ...
+spec:
+  servers:
+    - name: angus
+      fdw: postgres_fdw
+      ensure: present
+      usage:
+        - name: app
+          type: grant
+      options:
+        - name: host
+          value: angus-rw
+        - name: dbname
+          value: app
+# ...
+```
+
+Each foreign server entry supports the following properties:
+
+- `name`: The name of the foreign server **(mandatory)**.
+- `fdw`: The name of the foreign data wrapper the server belongs to
+  **(mandatory)**.
+- `ensure`: Whether the foreign server should be `present` or `absent` in the
+  database (default: `present`).
+- `usage`: The list of `USAGE` permissions of the foreign server, 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 **(mandatory)**.
+    - `type` : The type of the usage permission. Supports `grant` and `revoke`.
+- `options`: A list of FDW-specific option specifications.
+  Each entry in the list supports the following keys:
+    - `name`: The name of the option **(mandatory)**.
+    - `value`: The string value of the option.
+    - `ensure`: Indicates whether the option should be `present` or `absent`.
+
+!!! Important
+The `fdw` field must reference an existing foreign data wrapper already defined in the database.
+If the specified FDW does not exist, the foreign server will not be created.
+
+!!! Info
+CloudNativePG manages foreign servers using PostgreSQL’s native SQL commands:
+[`CREATE SERVER`](https://www.postgresql.org/docs/current/sql-createserver.html),
+[`ALTER SERVER`](https://www.postgresql.org/docs/current/sql-alterserver.html), and
+[`DROP SERVER`](https://www.postgresql.org/docs/current/sql-dropserver.html).
+The `ALTER SERVER` command is used to update server options.
+
+The operator reconciles **only** the foreign servers explicitly listed in
+`spec.servers`. Any existing servers not included in this list are left
+unchanged.
+
 ## Limitations and Caveats
 
 ### Renaming a database
diff --git a/docs/src/index.md b/docs/src/index.md
index 8baa489a..54415590 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -109,7 +109,8 @@ For details and support, see the [`postgres-containers` project](https://github.
 - Declarative management of key PostgreSQL configurations, including:
     - PostgreSQL settings.
     - Roles, users, and groups.
-    - Databases, extensions, schemas, and foreign data wrappers (FDW).
+    - Databases, extensions, schemas, foreign data wrappers (FDW), and foreign
+      servers.
     - 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 a73eba0d..e6acd618 100644
--- a/docs/src/operator_capability_levels.md
+++ b/docs/src/operator_capability_levels.md
@@ -148,8 +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, schemas and foreign data
-wrappers (FDW).
+the `Database` CRD, also supporting extensions, schemas, foreign data wrappers
+(FDW), and foreign servers.
 
 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/common.go b/internal/management/controller/common.go
index 51a3d30c..47a3bdd8 100644
--- a/internal/management/controller/common.go
+++ b/internal/management/controller/common.go
@@ -26,6 +26,7 @@ import (
 	"fmt"
 	"maps"
 	"slices"
+	"strings"
 	"time"
 
 	"github.com/cloudnative-pg/machinery/pkg/log"
@@ -173,3 +174,18 @@ func detectConflictingManagers[T postgresResourceManager, TL managedResourceExcl
 
 	return ctrl.Result{}, nil
 }
+
+func parseOptions(raw []string) (map[string]string, error) {
+	opts := make(map[string]string, len(raw))
+	for _, opt := range raw {
+		parts := strings.SplitN(opt, "=", 2)
+		if len(parts) != 2 {
+			return nil, fmt.Errorf(
+				"unparsable option: expected \"keyword=value\", got %v",
+				raw,
+			)
+		}
+		opts[parts[0]] = parts[1]
+	}
+	return opts, nil
+}
diff --git a/internal/management/controller/database_controller.go b/internal/management/controller/database_controller.go
index 7857506d..e26befe4 100644
--- a/internal/management/controller/database_controller.go
+++ b/internal/management/controller/database_controller.go
@@ -75,6 +75,14 @@ var fdwObjectManager = databaseObjectManager[apiv1.FDWSpec, fdwInfo]{
 	drop:   dropDatabaseFDW,
 }
 
+// serverObjectManager is the manager of the server objects
+var serverObjectManager = databaseObjectManager[apiv1.ServerSpec, serverInfo]{
+	get:    getDatabaseForeignServerInfo,
+	create: createDatabaseForeignServer,
+	update: updateDatabaseForeignServer,
+	drop:   dropDatabaseForeignServer,
+}
+
 // databaseReconciliationInterval is the time between the
 // database reconciliation loop failures
 const databaseReconciliationInterval = 30 * time.Second
@@ -256,6 +264,11 @@ func (r *DatabaseReconciler) reconcileDatabaseResource(ctx context.Context, obj
 			return ErrFailedDatabaseObjectReconciliation
 		}
 	}
+	for _, status := range obj.Status.Servers {
+		if !status.Applied {
+			return ErrFailedDatabaseObjectReconciliation
+		}
+	}
 
 	return nil
 }
@@ -264,10 +277,10 @@ func (r *DatabaseReconciler) reconcileDatabaseObjects(
 	ctx context.Context,
 	obj *apiv1.Database,
 ) error {
-	objectCount := 0
-	objectCount += len(obj.Spec.Schemas)
+	objectCount := len(obj.Spec.Schemas)
 	objectCount += len(obj.Spec.Extensions)
 	objectCount += len(obj.Spec.FDWs)
+	objectCount += len(obj.Spec.Servers)
 
 	if objectCount == 0 {
 		return nil
@@ -281,6 +294,7 @@ 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)
+	obj.Status.Servers = serverObjectManager.reconcileList(ctx, db, obj.Spec.Servers)
 
 	return nil
 }
diff --git a/internal/management/controller/database_controller_sql.go b/internal/management/controller/database_controller_sql.go
index 83289800..a20dc4aa 100644
--- a/internal/management/controller/database_controller_sql.go
+++ b/internal/management/controller/database_controller_sql.go
@@ -45,11 +45,17 @@ type schemaInfo struct {
 }
 
 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"`
+	Name      string            `json:"name"`
+	Handler   string            `json:"handler"`
+	Validator string            `json:"validator"`
+	Owner     string            `json:"owner"`
+	Options   map[string]string `json:"options"`
+}
+
+type serverInfo struct {
+	Name    string            `json:"name"`
+	FDWName string            `json:"fdwName"`
+	Options map[string]string `json:"options"`
 }
 
 func detectDatabase(
@@ -414,6 +420,50 @@ func dropDatabaseSchema(ctx context.Context, db *sql.DB, schema apiv1.SchemaSpec
 	return nil
 }
 
+// extractOptionsClauses takes a list of apiv1.OptionSpec and returns the present options as clauses
+// suitable to be joined with ", " inside a CREATE ... OPTIONS (...) statement.
+func extractOptionsClauses(options []apiv1.OptionSpec) []string {
+	opts := make([]string, 0, len(options))
+	for _, optionSpec := range options {
+		if optionSpec.Ensure == apiv1.EnsureAbsent {
+			continue
+		}
+		opts = append(opts, fmt.Sprintf("%s %s", pgx.Identifier{optionSpec.Name}.Sanitize(),
+			pq.QuoteLiteral(optionSpec.Value)))
+	}
+
+	return opts
+}
+
+// calculateAlterOptionsClauses returns the list of option alteration clauses (ADD / SET / DROP)
+// needed to reconcile the currentOptions of a database object with the desiredOptions.
+//
+// The returned slice is suitable to be joined with ", " inside an ALTER ... OPTIONS (...)
+// statement. Order of emitted clauses follows the order of desiredOptions, enabling
+// predictable application.
+func calculateAlterOptionsClauses(desiredOptions []apiv1.OptionSpec, currentOptions map[string]string) []string {
+	var clauses []string
+	for _, desiredOptSpec := range desiredOptions {
+		curOptValue, exists := currentOptions[desiredOptSpec.Name]
+
+		switch {
+		case desiredOptSpec.Ensure == apiv1.EnsurePresent && !exists:
+			clauses = append(clauses, fmt.Sprintf("ADD %s %s",
+				pgx.Identifier{desiredOptSpec.Name}.Sanitize(), pq.QuoteLiteral(desiredOptSpec.Value)))
+
+		case desiredOptSpec.Ensure == apiv1.EnsurePresent && exists && desiredOptSpec.Value != curOptValue:
+			clauses = append(clauses, fmt.Sprintf("SET %s %s",
+				pgx.Identifier{desiredOptSpec.Name}.Sanitize(), pq.QuoteLiteral(desiredOptSpec.Value)))
+
+		case desiredOptSpec.Ensure == apiv1.EnsureAbsent && exists:
+			clauses = append(clauses, fmt.Sprintf("DROP %s",
+				pgx.Identifier{desiredOptSpec.Name}.Sanitize()))
+		}
+	}
+
+	return clauses
+}
+
 const detectDatabaseFDWSQL = `
 SELECT
  fdwname, fdwhandler::regproc::text, fdwvalidator::regproc::text, fdwoptions,
@@ -424,21 +474,15 @@ 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 err := db.QueryRowContext(
+		ctx, detectDatabaseFDWSQL,
+		fdw.Name).
+		Scan(&result.Name, &result.Handler, &result.Validator, &optionsRaw, &result.Owner); err != nil {
 		if errors.Is(err, sql.ErrNoRows) {
 			return nil, nil
 		}
@@ -446,19 +490,9 @@ func getDatabaseFDWInfo(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) (*fd
 	}
 
 	// 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)
-		}
+	opts, err := parseOptions(optionsRaw)
+	if err != nil {
+		return nil, fmt.Errorf("while parsing options of foreign data wrapper %q: %w", fdw.Name, err)
 	}
 	result.Options = opts
 
@@ -467,36 +501,55 @@ func getDatabaseFDWInfo(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) (*fd
 
 // 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 {
+	const objectTypeForeignDataWrapper = "FOREIGN DATA WRAPPER"
+	return applyUsagePermissions(ctx, db, objectTypeForeignDataWrapper, fdw.Name, fdw.Usages)
+}
+
+// updateDatabaseForeignServerUsage updates the usage permissions of a foreign server in the database.
+// It supports granting or revoking usage permissions for specified users.
+func updateDatabaseForeignServerUsage(ctx context.Context, db *sql.DB, server *apiv1.ServerSpec) error {
+	const objectTypeForeignServer = "FOREIGN SERVER"
+	return applyUsagePermissions(ctx, db, objectTypeForeignServer, server.Name, server.Usages)
+}
+
+// applyUsagePermissions is a generic helper to grant or revoke USAGE permissions
+// for FOREIGN DATA WRAPPER / FOREIGN SERVER objects, avoiding duplicated logic.
+func applyUsagePermissions(
+	ctx context.Context,
+	db *sql.DB,
+	objectType string,
+	objectName string,
+	usages []apiv1.UsageSpec,
+) error {
 	contextLogger := log.FromContext(ctx)
 
-	for _, usageSpec := range fdw.Usages {
+	if len(usages) == 0 {
+		return nil
+	}
+
+	for _, usageSpec := range usages {
+		sanitizedObject := pgx.Identifier{objectName}.Sanitize()
+		sanitizedUser := pgx.Identifier{usageSpec.Name}.Sanitize()
+
 		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)
+		case apiv1.GrantUsageSpecType:
+			mutation := fmt.Sprintf("GRANT USAGE ON %s %s TO %s", objectType, sanitizedObject, sanitizedUser)
+			if _, err := db.ExecContext(ctx, mutation); err != nil {
+				return fmt.Errorf("granting usage of %s: %w", objectType, err)
 			}
-			contextLogger.Info("granted usage of foreign data wrapper", "name", fdw.Name, "user", usageSpec.Name)
+			contextLogger.Info("granted usage", "type", objectType, "name", objectName, "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)
+		case apiv1.RevokeUsageSpecType:
+			mutation := fmt.Sprintf("REVOKE USAGE ON %s %s FROM %s", objectType, sanitizedObject, sanitizedUser) // nolint:gosec
+			if _, err := db.ExecContext(ctx, mutation); err != nil {
+				return fmt.Errorf("revoking usage of %s: %w", objectType, err)
 			}
-			contextLogger.Info("revoked usage of foreign data wrapper", "name", fdw.Name, "user", usageSpec.Name)
+			contextLogger.Info("revoked usage", "type", objectType, "name", objectName, "user", usageSpec.Name)
 
 		default:
 			contextLogger.Warning(
-				"unknown usage type",
-				"type", usageSpec.Type, "fdwName", fdw.Name)
+				"unknown usage type", "type", usageSpec.Type, "objectType", objectType, "name", objectName)
 		}
 	}
 
@@ -529,16 +582,7 @@ func createDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) error
 		}
 	}
 
-	// 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 {
+	if opts := extractOptionsClauses(fdw.Options); len(opts) > 0 {
 		sqlCreateFDW.WriteString("OPTIONS (" + strings.Join(opts, ", ") + ")")
 	}
 
@@ -559,48 +603,6 @@ func createDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) error
 	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)
 
@@ -674,9 +676,16 @@ func updateDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec, info
 		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
+	if toUpdateOpts := calculateAlterOptionsClauses(fdw.Options, info.Options); len(toUpdateOpts) > 0 {
+		changeOptionSQL := fmt.Sprintf(
+			"ALTER FOREIGN DATA WRAPPER %s OPTIONS (%s)", pgx.Identifier{fdw.Name}.Sanitize(),
+			strings.Join(toUpdateOpts, ", "),
+		)
+
+		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)
 	}
 
 	// Update usage permissions
@@ -702,3 +711,105 @@ func dropDatabaseFDW(ctx context.Context, db *sql.DB, fdw apiv1.FDWSpec) error {
 	contextLogger.Info("dropped foreign data wrapper", "name", fdw.Name)
 	return nil
 }
+
+const detectDatabaseForeignServerSQL = `
+SELECT
+	srvname, fdwname, srvoptions
+FROM pg_foreign_server fs
+JOIN pg_foreign_data_wrapper fdw ON fs.srvfdw = fdw.oid
+WHERE srvname = $1
+`
+
+func getDatabaseForeignServerInfo(ctx context.Context, db *sql.DB, server apiv1.ServerSpec) (*serverInfo, error) {
+	var (
+		result     serverInfo
+		optionsRaw pq.StringArray
+	)
+
+	if err := db.QueryRowContext(
+		ctx, detectDatabaseForeignServerSQL,
+		server.Name).Scan(&result.Name, &result.FDWName, &optionsRaw); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("while scanning if foreign server %q exists: %w", server.Name, err)
+	}
+
+	opts, err := parseOptions(optionsRaw)
+	if err != nil {
+		return nil, fmt.Errorf("while parsing options of foreign server %q: %w", server.Name, err)
+	}
+	result.Options = opts
+
+	return &result, nil
+}
+
+// createDatabaseForeignServer creates a foreign server in the database.
+func createDatabaseForeignServer(ctx context.Context, db *sql.DB, server apiv1.ServerSpec) error {
+	contextLogger := log.FromContext(ctx)
+
+	var sqlCreateServer strings.Builder
+	sqlCreateServer.WriteString(fmt.Sprintf("CREATE SERVER %s FOREIGN DATA WRAPPER %s ",
+		pgx.Identifier{server.Name}.Sanitize(),
+		pgx.Identifier{server.FdwName}.Sanitize()))
+
+	if opts := extractOptionsClauses(server.Options); len(opts) > 0 {
+		sqlCreateServer.WriteString("OPTIONS (" + strings.Join(opts, ", ") + ")")
+	}
+
+	_, err := db.ExecContext(ctx, sqlCreateServer.String())
+	if err != nil {
+		contextLogger.Error(err, "while creating foreign server", "query", sqlCreateServer.String())
+		return err
+	}
+	contextLogger.Info("created foreign server", "name", server.Name)
+
+	if len(server.Usages) > 0 {
+		if err := updateDatabaseForeignServerUsage(ctx, db, &server); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// updateDatabaseForeignServer updates the configuration of a foreign server in the database.
+func updateDatabaseForeignServer(ctx context.Context, db *sql.DB, server apiv1.ServerSpec, info *serverInfo) error {
+	contextLogger := log.FromContext(ctx)
+
+	// Alter Options
+	if toUpdateOpts := calculateAlterOptionsClauses(server.Options, info.Options); len(toUpdateOpts) > 0 {
+		changeOptionSQL := fmt.Sprintf(
+			"ALTER SERVER %s OPTIONS (%s)", pgx.Identifier{server.Name}.Sanitize(),
+			strings.Join(toUpdateOpts, ", "),
+		)
+
+		if _, err := db.ExecContext(ctx, changeOptionSQL); err != nil {
+			return fmt.Errorf("altering options of foreign server %w", err)
+		}
+		contextLogger.Info("altered foreign server options", "name", server.Name, "options", server.Options)
+	}
+
+	if len(server.Usages) > 0 {
+		if err := updateDatabaseForeignServerUsage(ctx, db, &server); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// dropDatabaseForeignServer drops a foreign server from the database.
+func dropDatabaseForeignServer(ctx context.Context, db *sql.DB, server apiv1.ServerSpec) error {
+	contextLogger := log.FromContext(ctx)
+	query := fmt.Sprintf("DROP SERVER IF EXISTS %s", pgx.Identifier{server.Name}.Sanitize())
+	_, err := db.ExecContext(
+		ctx,
+		query)
+	if err != nil {
+		contextLogger.Error(err, "while dropping foreign server", "query", query)
+		return err
+	}
+	contextLogger.Info("dropped foreign server", "name", server.Name)
+	return nil
+}
diff --git a/internal/webhook/v1/database_webhook.go b/internal/webhook/v1/database_webhook.go
index f4250dc6..8bcf706c 100644
--- a/internal/webhook/v1/database_webhook.go
+++ b/internal/webhook/v1/database_webhook.go
@@ -155,6 +155,7 @@ func (v *DatabaseCustomValidator) validate(d *apiv1.Database) (allErrs field.Err
 		v.validateExtensions,
 		v.validateSchemas,
 		v.validateFDWs,
+		v.validateForeignServers,
 	}
 
 	for _, validate := range validations {
@@ -196,8 +197,8 @@ func (v *DatabaseCustomValidator) validateSchemas(d *apiv1.Database) field.Error
 	var result field.ErrorList
 
 	schemaNames := stringset.New()
-	for i, schema := range d.Spec.Schemas {
-		name := schema.Name
+	for i, schemaSpec := range d.Spec.Schemas {
+		name := schemaSpec.Name
 		if schemaNames.Has(name) {
 			result = append(
 				result,
@@ -218,56 +219,87 @@ func (v *DatabaseCustomValidator) validateSchemas(d *apiv1.Database) field.Error
 // FDWs must be unique in .spec.fdws
 func (v *DatabaseCustomValidator) validateFDWs(d *apiv1.Database) field.ErrorList {
 	var result field.ErrorList
+	nameSet := stringset.New()
+	basePath := field.NewPath("spec", "fdws")
+	for i, fdw := range d.Spec.FDWs {
+		itemPath := basePath.Index(i)
+		errs := validateNameOptionsUsages(itemPath, fdw.Name, fdw.Options, fdw.Usages, nameSet)
+		result = append(result, errs...)
+	}
+	return result
+}
+
+// validateForeignServers validates foreign servers: uniqueness, options/usages duplicates,
+// and that each referenced FDW exists.
+func (v *DatabaseCustomValidator) validateForeignServers(d *apiv1.Database) field.ErrorList {
+	basePath := field.NewPath("spec", "servers")
 
 	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,
-				),
-			)
-		}
+	for _, fdw := range d.Spec.FDWs {
+		fdwNames.Put(fdw.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)
-		}
+	nameSet := stringset.New()
+	var allErrs field.ErrorList
+	for i, server := range d.Spec.Servers {
+		itemPath := basePath.Index(i)
 
-		// 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)
+		allErrs = append(allErrs, v.validateServerFDWReference(fdwNames, server, itemPath)...)
+
+		allErrs = append(allErrs,
+			validateNameOptionsUsages(itemPath, server.Name, server.Options, server.Usages, nameSet)...)
+	}
+
+	return allErrs
+}
+
+// validateServerFDWReference ensures the server references an existing FDW (and is non-empty).
+func (v *DatabaseCustomValidator) validateServerFDWReference(
+	fdwNames *stringset.Data,
+	server apiv1.ServerSpec,
+	itemPath *field.Path,
+) field.ErrorList {
+	if server.GetEnsure() == apiv1.EnsureAbsent || fdwNames.Has(server.FdwName) {
+		return nil
+	}
+
+	return field.ErrorList{field.Invalid(
+		itemPath.Child("fdw"),
+		server.FdwName,
+		"referenced fdw not defined in spec.fdws",
+	)}
+}
+
+// validateNameOptionsUsages validates a single named object with options and usages, tracking duplicates.
+func validateNameOptionsUsages(
+	itemPath *field.Path,
+	name string,
+	options []apiv1.OptionSpec,
+	usages []apiv1.UsageSpec,
+	existingNames *stringset.Data,
+) field.ErrorList {
+	var errs field.ErrorList
+
+	if existingNames.Has(name) {
+		errs = append(errs, field.Duplicate(itemPath.Child("name"), name))
+	}
+	existingNames.Put(name)
+
+	optionNames := stringset.New()
+	for i, option := range options {
+		if optionNames.Has(option.Name) {
+			errs = append(errs, field.Duplicate(itemPath.Child("options").Index(i).Child("name"), option.Name))
 		}
+		optionNames.Put(option.Name)
+	}
 
-		fdwNames.Put(name)
+	usageNames := stringset.New()
+	for i, usage := range usages {
+		if usageNames.Has(usage.Name) {
+			errs = append(errs, field.Duplicate(itemPath.Child("usages").Index(i).Child("name"), usage.Name))
+		}
+		usageNames.Put(usage.Name)
 	}
 
-	return result
+	return errs
 }



Test patch:
diff --git a/internal/management/controller/database_controller_sql_test.go b/internal/management/controller/database_controller_sql_test.go
index f520039f..afcefa78 100644
--- a/internal/management/controller/database_controller_sql_test.go
+++ b/internal/management/controller/database_controller_sql_test.go
@@ -540,10 +540,12 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 			Validator: "testvalidator",
 			Options: []apiv1.OptionSpec{
 				{
-					Name: "testoption",
-					OptionSpecValue: apiv1.OptionSpecValue{
-						Value: "testvalue",
-					},
+					Name:  "testoption",
+					Value: "testvalue",
+				},
+				{
+					Name:  "testoption2",
+					Value: "testvalue2",
 				},
 			},
 			Owner: "owner",
@@ -557,7 +559,7 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 	})
 
 	Context("getDatabaseFDWInfo", func() {
-		It("returns info when the fdw exits", func(ctx SpecContext) {
+		It("returns info when the fdw exists", func(ctx SpecContext) {
 			dbMock.
 				ExpectQuery(detectDatabaseFDWSQL).
 				WithArgs(fdw.Name).
@@ -589,7 +591,7 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 
 	Context("createDatabaseFDW", func() {
 		createFDWSQL := "CREATE FOREIGN DATA WRAPPER \"testfdw\" HANDLER \"testhandler\" " +
-			"VALIDATOR \"testvalidator\" OPTIONS (\"testoption\" 'testvalue')"
+			"VALIDATOR \"testvalidator\" OPTIONS (\"testoption\" 'testvalue', \"testoption2\" 'testvalue2')"
 
 		It("returns success when the fdw has been created", func(ctx SpecContext) {
 			dbMock.
@@ -697,20 +699,18 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 		It("add new fdw options", func(ctx SpecContext) {
 			fdw.Options = []apiv1.OptionSpec{
 				{
-					Name: "add_option",
-					OptionSpecValue: apiv1.OptionSpecValue{
-						Value:  "value",
-						Ensure: apiv1.EnsurePresent,
-					},
+					Name:   "add_option",
+					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"},
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
 				},
 				Owner: fdw.Owner,
 			}
@@ -724,20 +724,18 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 		It("modify the fdw options", func(ctx SpecContext) {
 			fdw.Options = []apiv1.OptionSpec{
 				{
-					Name: "modify_option",
-					OptionSpecValue: apiv1.OptionSpecValue{
-						Value:  "new_value",
-						Ensure: apiv1.EnsurePresent,
-					},
+					Name:   "modify_option",
+					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"},
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
 				},
 				Owner: fdw.Owner,
 			}
@@ -751,20 +749,18 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 		It("remove new fdw options", func(ctx SpecContext) {
 			fdw.Options = []apiv1.OptionSpec{
 				{
-					Name: "remove_option",
-					OptionSpecValue: apiv1.OptionSpecValue{
-						Value:  "value",
-						Ensure: apiv1.EnsureAbsent,
-					},
+					Name:   "remove_option",
+					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"},
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
 				},
 				Owner: fdw.Owner,
 			}
@@ -828,3 +824,195 @@ var _ = Describe("Managed Foreign Data Wrapper SQL", func() {
 		})
 	})
 })
+
+var _ = Describe("Managed Foreign Server SQL", func() {
+	var (
+		dbMock sqlmock.Sqlmock
+		db     *sql.DB
+		server apiv1.ServerSpec
+		err    error
+
+		testError error
+	)
+
+	BeforeEach(func() {
+		db, dbMock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		Expect(err).ToNot(HaveOccurred())
+
+		server = apiv1.ServerSpec{
+			DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
+				Name:   "testserver",
+				Ensure: "present",
+			},
+			FdwName: "testfdw",
+			Options: []apiv1.OptionSpec{
+				{Name: "host", Value: "localhost"},
+				{Name: "port", Value: "5432"},
+			},
+		}
+
+		testError = fmt.Errorf("test error")
+	})
+
+	AfterEach(func() {
+		Expect(dbMock.ExpectationsWereMet()).To(Succeed())
+	})
+
+	Context("getDatabaseForeignServerInfo", func() {
+		It("returns info when the foreign server exists", func(ctx SpecContext) {
+			dbMock.
+				ExpectQuery(detectDatabaseForeignServerSQL).
+				WithArgs(server.Name).
+				WillReturnRows(
+					sqlmock.NewRows([]string{"servername", "fdwname", "options"}).
+						AddRow("testserver", "testfdw", nil),
+				)
+			serverInfo, err := getDatabaseForeignServerInfo(ctx, db, server)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(serverInfo).ToNot(BeNil())
+			Expect(serverInfo.Name).To(Equal("testserver"))
+			Expect(serverInfo.FDWName).To(Equal("testfdw"))
+		})
+
+		It("returns nil info when the foreign server does not exiest", func(ctx SpecContext) {
+			dbMock.
+				ExpectQuery(detectDatabaseForeignServerSQL).
+				WithArgs(server.Name).
+				WillReturnRows(
+					sqlmock.NewRows([]string{"servername", "fdwname"}),
+				)
+			serverInfo, err := getDatabaseForeignServerInfo(ctx, db, server)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(serverInfo).To(BeNil())
+		})
+	})
+
+	Context("createDatabaseForeignServer", func() {
+		createForeignServerSQL := "CREATE SERVER \"testserver\" FOREIGN DATA WRAPPER \"testfdw\"" +
+			" OPTIONS (\"host\" 'localhost', \"port\" '5432')"
+
+		It("returns success when the foreign server has been created", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(createForeignServerSQL).
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			Expect(createDatabaseForeignServer(ctx, db, server)).Error().NotTo(HaveOccurred())
+		})
+
+		It("fails when the foreign server could not be created", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(createForeignServerSQL).
+				WillReturnError(testError)
+			Expect(createDatabaseForeignServer(ctx, db, server)).Error().To(Equal(testError))
+		})
+	})
+
+	Context("updateDatabaseForeignServer", func() {
+		It("does nothing when the foreign server does not exist", func(ctx SpecContext) {
+			Expect(updateDatabaseForeignServer(ctx, db, server, &serverInfo{
+				Name:    server.Name,
+				FDWName: server.FdwName,
+			})).Error().NotTo(HaveOccurred())
+		})
+
+		It("add new foreign server options", func(ctx SpecContext) {
+			server.Options = []apiv1.OptionSpec{
+				{
+					Name:   "add_option",
+					Value:  "value",
+					Ensure: apiv1.EnsurePresent,
+				},
+			}
+			info := &serverInfo{
+				Name:    server.Name,
+				FDWName: server.FdwName,
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
+				},
+			}
+
+			expectedSQL := "ALTER SERVER \"testserver\" OPTIONS (ADD \"add_option\" 'value')"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseForeignServer(ctx, db, server, info)).Error().NotTo(HaveOccurred())
+		})
+
+		It("modify the foreign server options", func(ctx SpecContext) {
+			server.Options = []apiv1.OptionSpec{
+				{
+					Name:   "modify_option",
+					Value:  "new_value",
+					Ensure: apiv1.EnsurePresent,
+				},
+			}
+			info := &serverInfo{
+				Name:    server.Name,
+				FDWName: server.FdwName,
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
+				},
+			}
+
+			expectedSQL := "ALTER SERVER \"testserver\" OPTIONS (SET \"modify_option\" 'new_value')"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseForeignServer(ctx, db, server, info)).Error().NotTo(HaveOccurred())
+		})
+
+		It("updates the usages permissions of the foreign server", func(ctx SpecContext) {
+			dbMock.ExpectExec(
+				"GRANT USAGE ON FOREIGN SERVER \"testserver\" TO \"owner\"").
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			server.Usages = []apiv1.UsageSpec{
+				{
+					Name: "owner",
+					Type: "grant",
+				},
+			}
+			Expect(updateDatabaseForeignServerUsage(ctx, db, &server)).Error().NotTo(HaveOccurred())
+		})
+
+		It("remove foreign server options", func(ctx SpecContext) {
+			server.Options = []apiv1.OptionSpec{
+				{
+					Name:   "remove_option",
+					Value:  "value",
+					Ensure: apiv1.EnsureAbsent,
+				},
+			}
+			info := &serverInfo{
+				Name:    server.Name,
+				FDWName: server.FdwName,
+				Options: map[string]string{
+					"modify_option": "old_value",
+					"remove_option": "value",
+				},
+			}
+
+			expectedSQL := "ALTER SERVER \"testserver\" OPTIONS (DROP \"remove_option\")"
+			dbMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 1))
+
+			Expect(updateDatabaseForeignServer(ctx, db, server, info)).Error().NotTo(HaveOccurred())
+		})
+	})
+
+	Context("dropDatabaseForeignServer", func() {
+		dropForeignServerSQL := "DROP SERVER IF EXISTS \"testserver\""
+
+		It("returns success when the foreign server has been dropped", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(dropForeignServerSQL).
+				WillReturnResult(sqlmock.NewResult(0, 1))
+			Expect(dropDatabaseForeignServer(ctx, db, server)).Error().NotTo(HaveOccurred())
+		})
+
+		It("returns an error when the DROP statement failed", func(ctx SpecContext) {
+			dbMock.
+				ExpectExec(dropForeignServerSQL).
+				WillReturnError(testError)
+
+			Expect(dropDatabaseForeignServer(ctx, db, server)).Error().To(Equal(testError))
+		})
+	})
+})
diff --git a/internal/webhook/v1/database_webhook_test.go b/internal/webhook/v1/database_webhook_test.go
index cebb1d24..4d5a3edf 100644
--- a/internal/webhook/v1/database_webhook_test.go
+++ b/internal/webhook/v1/database_webhook_test.go
@@ -20,6 +20,8 @@ SPDX-License-Identifier: Apache-2.0
 package v1
 
 import (
+	"k8s.io/apimachinery/pkg/util/validation/field"
+
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -45,7 +47,6 @@ var _ = Describe("Database validation", func() {
 			},
 		}
 	}
-
 	createFDWSpec := func(name string) apiv1.FDWSpec {
 		return apiv1.FDWSpec{
 			DatabaseObjectSpec: apiv1.DatabaseObjectSpec{
@@ -54,169 +55,268 @@ var _ = Describe("Database validation", func() {
 			},
 		}
 	}
+
+	// helper to extract the field paths (e.g. spec.extensions[1].name) from the error list
+	extractErrorFields := func(errs field.ErrorList) []string {
+		res := make([]string, 0, len(errs))
+		for _, e := range errs {
+			res = append(res, e.Field)
+		}
+		return res
+	}
+
+	// expectDuplicateErrors validates that we have exactly the expected duplicate errors with correct bad values
+	expectDuplicateErrors := func(errs field.ErrorList, expected map[string]string) {
+		Expect(errs).To(HaveLen(len(expected)))
+		for path, val := range expected {
+			var found *field.Error
+			for i := range errs {
+				if errs[i].Field == path {
+					found = errs[i]
+					break
+				}
+			}
+			Expect(found).To(HaveOccurred(), "missing duplicate error for %s", path)
+			Expect(found.Type).To(Equal(field.ErrorTypeDuplicate), "expected duplicate error type for %s", path)
+			Expect(found.BadValue).To(Equal(val), "unexpected bad value for %s", path)
+		}
+	}
+
 	BeforeEach(func() {
 		v = &DatabaseCustomValidator{}
 	})
 
-	DescribeTable(
-		"Database validation",
-		func(db *apiv1.Database, errorCount int) {
-			foundErrors := v.validate(db)
-			Expect(foundErrors).To(HaveLen(errorCount))
-		},
-		Entry(
-			"doesn't complain when extensions and schemas are null",
-			&apiv1.Database{
-				Spec: apiv1.DatabaseSpec{},
+	It("doesn't complain when extensions and schemas are null", func() {
+		db := &apiv1.Database{Spec: apiv1.DatabaseSpec{}}
+		errs := v.validate(db)
+		Expect(errs).To(BeEmpty())
+	})
+
+	It("doesn't complain if there are no duplicate extensions and no duplicate schemas", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				Extensions: []apiv1.ExtensionSpec{
+					createExtensionSpec("postgis"),
+				},
+				Schemas: []apiv1.SchemaSpec{
+					createSchemaSpec("test_schema"),
+				},
+			},
+		}
+		errs := v.validate(db)
+		Expect(errs).To(BeEmpty())
+	})
+
+	It("complains if there are duplicate extensions", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				Extensions: []apiv1.ExtensionSpec{
+					createExtensionSpec("postgis"),
+					createExtensionSpec("postgis"),
+					createExtensionSpec("cube"),
+				},
+			},
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.extensions[1].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.extensions[1].name": "postgis"})
+	})
+
+	It("complains if there are duplicate schemas", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				Schemas: []apiv1.SchemaSpec{
+					createSchemaSpec("test_one"),
+					createSchemaSpec("test_two"),
+					createSchemaSpec("test_two"),
+				},
 			},
-			0,
-		),
-		Entry(
-			"doesn't complain if there are no duplicate extensions and no duplicate schemas",
-			&apiv1.Database{
-				Spec: apiv1.DatabaseSpec{
-					Extensions: []apiv1.ExtensionSpec{
-						createExtensionSpec("postgis"),
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.schemas[2].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.schemas[2].name": "test_two"})
+	})
+
+	It("doesn't complain with distinct FDWs and usage names", func() {
+		db := &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"}},
 					},
-					Schemas: []apiv1.SchemaSpec{
-						createSchemaSpec("test_schema"),
+					{
+						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 extensions",
-			&apiv1.Database{
-				Spec: apiv1.DatabaseSpec{
-					Extensions: []apiv1.ExtensionSpec{
-						createExtensionSpec("postgis"),
-						createExtensionSpec("postgis"),
-						createExtensionSpec("cube"),
+		}
+		errs := v.validate(db)
+		Expect(errs).To(BeEmpty())
+	})
+
+	It("complains if there are duplicate FDWs", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{
+					createFDWSpec("postgres_fdw"),
+					createFDWSpec("mysql_fdw"),
+					createFDWSpec("postgres_fdw"),
+				},
+			},
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.fdws[2].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.fdws[2].name": "postgres_fdw"})
+	})
+
+	It("complains if there are duplicate usage names within an FDW", func() {
+		db := &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(
-			"complain if there are duplicate schemas",
-			&apiv1.Database{
-				Spec: apiv1.DatabaseSpec{
-					Schemas: []apiv1.SchemaSpec{
-						createSchemaSpec("test_one"),
-						createSchemaSpec("test_two"),
-						createSchemaSpec("test_two"),
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.fdws[0].usages[2].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.fdws[0].usages[2].name": "usage1"})
+	})
+
+	It("complains for duplicate FDW and duplicate usage and option names", func() { // consolidated wording
+		db := &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},
 					},
 				},
 			},
-			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"},
-							},
-						},
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf(
+			"spec.fdws[0].options[1].name",
+			"spec.fdws[0].usages[1].name",
+			"spec.fdws[1].name",
+		))
+		expectDuplicateErrors(errs, map[string]string{
+			"spec.fdws[0].options[1].name": "dup_option",
+			"spec.fdws[0].usages[1].name":  "dup_usage",
+			"spec.fdws[1].name":            "duplicate_fdw",
+		})
+	})
+
+	It("doesn't complain with distinct foreign servers", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{
+					{
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw1"},
+					},
+					{
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw2"},
 					},
 				},
+				Servers: []apiv1.ServerSpec{
+					{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server1", Ensure: apiv1.EnsurePresent}, FdwName: "fdw1"},
+					{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server2", Ensure: apiv1.EnsurePresent}, FdwName: "fdw2"},
+				},
+			},
+		}
+		errs := v.validate(db)
+		Expect(errs).To(BeEmpty())
+	})
+
+	It("complains for duplicate foreign servers", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw1"}}},
+				Servers: []apiv1.ServerSpec{
+					{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "dup_server", Ensure: apiv1.EnsurePresent}, FdwName: "fdw1"},
+					{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "dup_server", Ensure: apiv1.EnsurePresent}, FdwName: "fdw1"},
+				},
 			},
-			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"),
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.servers[1].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.servers[1].name": "dup_server"})
+	})
+
+	It("complains for duplicate options within a single foreign server", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw1"}}},
+				Servers: []apiv1.ServerSpec{
+					{
+						FdwName:            "fdw1",
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server1", Ensure: apiv1.EnsurePresent},
+						Options:            []apiv1.OptionSpec{{Name: "duplicate_option"}, {Name: "duplicate_option"}},
 					},
 				},
 			},
-			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"},
-							},
-						},
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.servers[0].options[1].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.servers[0].options[1].name": "duplicate_option"})
+	})
+
+	It("complains for duplicate usages within a single foreign server", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw1"}}},
+				Servers: []apiv1.ServerSpec{
+					{
+						FdwName:            "fdw1",
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server1", Ensure: apiv1.EnsurePresent},
+						Usages:             []apiv1.UsageSpec{{Name: "duplicate_usage"}, {Name: "duplicate_usage"}},
 					},
 				},
 			},
-			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,
-							},
-						},
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf("spec.servers[0].usages[1].name"))
+		expectDuplicateErrors(errs, map[string]string{"spec.servers[0].usages[1].name": "duplicate_usage"})
+	})
+
+	It("complains for a combination of duplicate servers, options, and usages", func() {
+		db := &apiv1.Database{
+			Spec: apiv1.DatabaseSpec{
+				FDWs: []apiv1.FDWSpec{{DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "fdw1"}}},
+				Servers: []apiv1.ServerSpec{
+					{
+						FdwName:            "fdw1",
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server1", Ensure: apiv1.EnsurePresent},
+						Options:            []apiv1.OptionSpec{{Name: "dup_option"}, {Name: "dup_option"}},
+						Usages:             []apiv1.UsageSpec{{Name: "dup_usage"}, {Name: "dup_usage"}},
+					},
+					{
+						FdwName:            "fdw1",
+						DatabaseObjectSpec: apiv1.DatabaseObjectSpec{Name: "server1", Ensure: apiv1.EnsurePresent},
 					},
 				},
 			},
-			3,
-		),
-	)
+		}
+		errs := v.validate(db)
+		Expect(extractErrorFields(errs)).To(ConsistOf(
+			"spec.servers[0].options[1].name",
+			"spec.servers[0].usages[1].name",
+			"spec.servers[1].name",
+		))
+		expectDuplicateErrors(errs, map[string]string{
+			"spec.servers[0].options[1].name": "dup_option",
+			"spec.servers[0].usages[1].name":  "dup_usage",
+			"spec.servers[1].name":            "server1",
+		})
+	})
 })
diff --git a/tests/e2e/asserts_test.go b/tests/e2e/asserts_test.go
index 9ba006e1..ad4813db 100644
--- a/tests/e2e/asserts_test.go
+++ b/tests/e2e/asserts_test.go
@@ -557,6 +557,10 @@ func fdwExistsQuery(fdwName string) string {
 	return fmt.Sprintf("SELECT EXISTS(SELECT FROM pg_catalog.pg_foreign_data_wrapper WHERE fdwname='%v')", fdwName)
 }
 
+func foreignServerExistsQuery(serverName string) string {
+	return fmt.Sprintf("SELECT EXISTS(SELECT FROM pg_catalog.pg_foreign_server WHERE srvname='%v')", serverName)
+}
+
 // 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 9ccebdf6..a7cdfee3 100644
--- a/tests/e2e/declarative_database_management_test.go
+++ b/tests/e2e/declarative_database_management_test.go
@@ -164,6 +164,16 @@ var _ = Describe("Declarative database management", Label(tests.LabelSmoke, test
 				}
 			})
 
+			By("verifying the foreign server presence in the target database", func() {
+				primaryPodInfo, err := clusterutils.GetPrimary(env.Ctx, env.Client, namespace, clusterName)
+				Expect(err).ToNot(HaveOccurred())
+
+				for _, serverSpec := range database.Spec.Servers {
+					Eventually(QueryMatchExpectationPredicate(primaryPodInfo, exec.DatabaseName(database.Spec.Name),
+						foreignServerExistsQuery(serverSpec.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 ec59c21a..1b48fcab 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
@@ -12,22 +12,26 @@ spec:
   cluster:
     name: cluster-with-declarative-databases
   extensions:
-  - name: bloom
-    ensure: present
-  - name: postgres_fdw
-    ensure: present
+    - name: bloom
+      ensure: present
+    - name: postgres_fdw
+      ensure: present
   schemas:
-  - name: test_schema
-    ensure: present
+    - name: test_schema
+      ensure: present
   fdws:
     - name: postgres_fdw
       ensure: present
       usage:
-      - name: app
-        type: grant
+        - name: app
+          type: grant
     - name: mywrapper
       options:
         - name: debug
           value: 'true'
           ensure: present
       ensure: present
+  servers:
+    - name: server1
+      fdw: postgres_fdw
+      ensure: present
diff --git a/tests/e2e/fixtures/declarative_databases/database.yaml.template b/tests/e2e/fixtures/declarative_databases/database.yaml.template
index 1d67109a..e2848983 100644
--- a/tests/e2e/fixtures/declarative_databases/database.yaml.template
+++ b/tests/e2e/fixtures/declarative_databases/database.yaml.template
@@ -12,22 +12,26 @@ spec:
   cluster:
     name: cluster-with-declarative-databases
   extensions:
-  - name: bloom
-    ensure: present
-  - name: postgres_fdw
-    ensure: present
+    - name: bloom
+      ensure: present
+    - name: postgres_fdw
+      ensure: present
   schemas:
-  - name: test_schema
-    ensure: present
+    - name: test_schema
+      ensure: present
   fdws:
     - name: postgres_fdw
       ensure: present
       usage:
-      - name: app
-        type: grant
+        - name: app
+          type: grant
     - name: mywrapper
       options:
         - name: debug
           value: 'true'
           ensure: present
       ensure: present
+  servers:
+    - name: server1
+      fdw: postgres_fdw
+      ensure: present