diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7b7f43da..972eacc4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 
 ## [Unreleased]
 
+### Added
+
+- More CRD documentation ([#697]).
+
+[#697]: https://github.com/stackabletech/operator-rs/pull/697
+
 ## [0.58.0] - 2023-12-04
 
 ### Added
diff --git a/src/commons/authentication/ldap.rs b/src/commons/authentication/ldap.rs
index 45daf70e4..069140b23 100644
--- a/src/commons/authentication/ldap.rs
+++ b/src/commons/authentication/ldap.rs
@@ -13,28 +13,28 @@ use crate::{
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct AuthenticationProvider {
-    /// Hostname of the LDAP server
+    /// Hostname of the LDAP server, for example: `my.ldap.server`.
     pub hostname: String,
 
-    /// Port of the LDAP server. If TLS is used defaults to 636 otherwise to 389
+    /// Port of the LDAP server. If TLS is used defaults to 636 otherwise to 389.
     port: Option<u16>,
 
-    /// LDAP search base
+    /// LDAP search base, for example: `ou=users,dc=example,dc=org`.
     #[serde(default)]
     pub search_base: String,
 
-    /// LDAP query to filter users
+    /// LDAP query to filter users, for example: `(memberOf=cn=myTeam,ou=teams,dc=example,dc=org)`.
     #[serde(default)]
     pub search_filter: String,
 
-    /// The name of the LDAP object fields
+    /// The name of the LDAP object fields.
     #[serde(default)]
     pub ldap_field_names: FieldNames,
 
-    /// In case you need a special account for searching the LDAP server you can specify it here
+    /// In case you need a special account for searching the LDAP server you can specify it here.
     bind_credentials: Option<SecretClassVolume>,
 
-    /// Use a TLS connection. If not specified no TLS will be used
+    /// Use a TLS connection. If not specified no TLS will be used.
     #[serde(flatten)]
     pub tls: TlsClientDetails,
 }
diff --git a/src/commons/authentication/mod.rs b/src/commons/authentication/mod.rs
index 7203196e5..1837870c2 100644
--- a/src/commons/authentication/mod.rs
+++ b/src/commons/authentication/mod.rs
@@ -15,6 +15,11 @@ pub mod tls;
 
 pub(crate) const SECRET_BASE_PATH: &str = "/stackable/secrets";
 
+/// The Stackable Platform uses the AuthenticationClass as a central mechanism to handle user authentication across supported products.
+/// The authentication mechanism needs to be configured only in the AuthenticationClass which is then referenced in the product.
+/// Multiple different authentication providers are supported.
+/// Learn more in the [authentication concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication) and the
+/// [Authentication with OpenLDAP tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap).
 #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[kube(
     group = "authentication.stackable.tech",
@@ -29,7 +34,7 @@ pub(crate) const SECRET_BASE_PATH: &str = "/stackable/secrets";
 )]
 #[serde(rename_all = "camelCase")]
 pub struct AuthenticationClassSpec {
-    /// Provider used for authentication like LDAP or Kerberos
+    /// Provider used for authentication like LDAP or Kerberos.
     pub provider: AuthenticationClassProvider,
 }
 
@@ -37,9 +42,20 @@ pub struct AuthenticationClassSpec {
 #[serde(rename_all = "camelCase")]
 #[allow(clippy::large_enum_variant)]
 pub enum AuthenticationClassProvider {
+    /// The [static provider](https://DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_static) is used to configure a
+    /// static set of users, identified by username and password.
     Static(static_::AuthenticationProvider),
+
+    /// The [LDAP provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_ldap).
+    /// There is also the ["Authentication with LDAP" tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap)
+    /// where you can learn to configure Superset and Trino with OpenLDAP.
     Ldap(ldap::AuthenticationProvider),
+
+    /// The OIDC provider can be used to configure OpenID Connect.
     Oidc(oidc::AuthenticationProvider),
+
+    /// The [TLS provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_tls).
+    /// The TLS AuthenticationClass is used when users should authenticate themselves with a TLS certificate.
     Tls(tls::AuthenticationProvider),
 }
 
diff --git a/src/commons/authentication/oidc.rs b/src/commons/authentication/oidc.rs
index 8d2e3413e..1581ae2f4 100644
--- a/src/commons/authentication/oidc.rs
+++ b/src/commons/authentication/oidc.rs
@@ -41,8 +41,8 @@ pub struct AuthenticationProvider {
     /// Hostname of the identity provider, e.g. `my.keycloak.corp`.
     hostname: String,
 
-    /// Port of the identity provider. If TLS is used defaults to `443`,
-    /// otherwise to `80`.
+    /// Port of the identity provider. If TLS is used defaults to 443,
+    /// otherwise to 80.
     port: Option<u16>,
 
     /// Root HTTP path of the identity provider. Defaults to `/`.
@@ -74,9 +74,9 @@ pub struct AuthenticationProvider {
     pub scopes: Vec<String>,
 
     /// This is a hint about which identity provider is used by the
-    /// [`AuthenticationClass`]. Operators *can* opt to use this
+    /// AuthenticationClass. Operators *can* opt to use this
     /// value to enable known quirks around OIDC / OAuth authentication.
-    /// [`None`] means there is no hint and OIDC should be used as it is
+    /// Not providing a hint means there is no hint and OIDC should be used as it is
     /// intended to be used (via the `.well-known` discovery).
     #[serde(default)]
     pub provider_hint: Option<IdentityProviderHint>,
diff --git a/src/commons/authentication/static_.rs b/src/commons/authentication/static_.rs
index 19deaa9ee..153d61185 100644
--- a/src/commons/authentication/static_.rs
+++ b/src/commons/authentication/static_.rs
@@ -25,8 +25,8 @@ use serde::{Deserialize, Serialize};
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct AuthenticationProvider {
-    /// Secret providing the usernames and password.
-    /// The secret must contain an entry for every user, with the key being the username and the value the password in plain text.
+    /// Secret providing the usernames and passwords.
+    /// The Secret must contain an entry for every user, with the key being the username and the value the password in plain text.
     /// It must be located in the same namespace as the product using it.
     pub user_credentials_secret: UserCredentialsSecretRef,
 }
@@ -34,6 +34,6 @@ pub struct AuthenticationProvider {
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct UserCredentialsSecretRef {
-    /// Name of the secret
+    /// Name of the Secret.
     pub name: String,
 }
diff --git a/src/commons/authentication/tls.rs b/src/commons/authentication/tls.rs
index dfa7060e6..be51ca24b 100644
--- a/src/commons/authentication/tls.rs
+++ b/src/commons/authentication/tls.rs
@@ -10,9 +10,9 @@ use crate::{
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct AuthenticationProvider {
-    /// See `<https://docs.stackable.tech/home/contributor/adr/ADR016-tls-authentication.html>`.
+    /// See [ADR017: TLS authentication](DOCS_BASE_URL_PLACEHOLDER/contributor/adr/adr017-tls_authentication).
     /// If `client_cert_secret_class` is not set, the TLS settings may also be used for client authentication.
-    /// If `client_cert_secret_class` is set, the [SecretClass](https://docs.stackable.tech/secret-operator/secretclass.html)
+    /// If `client_cert_secret_class` is set, the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass)
     /// will be used to provision client certificates.
     pub client_cert_secret_class: Option<String>,
 }
@@ -20,7 +20,7 @@ pub struct AuthenticationProvider {
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct TlsClientDetails {
-    /// Use a TLS connection. If not specified no TLS will be used
+    /// Use a TLS connection. If not specified no TLS will be used.
     pub tls: Option<Tls>,
 }
 
@@ -108,36 +108,36 @@ impl TlsClientDetails {
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Tls {
-    /// The verification method used to verify the certificates of the server and/or the client
+    /// The verification method used to verify the certificates of the server and/or the client.
     pub verification: TlsVerification,
 }
 
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum TlsVerification {
-    /// Use TLS but don't verify certificates
+    /// Use TLS but don't verify certificates.
     None {},
 
-    /// Use TLS and ca certificate to verify the server
+    /// Use TLS and a CA certificate to verify the server.
     Server(TlsServerVerification),
 }
 
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct TlsServerVerification {
-    /// Ca cert to verify the server
+    /// CA cert to verify the server.
     pub ca_cert: CaCert,
 }
 
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum CaCert {
-    /// Use TLS and the ca certificates trusted by the common web browsers to verify the server.
+    /// Use TLS and the CA certificates trusted by the common web browsers to verify the server.
     /// This can be useful when you e.g. use public AWS S3 or other public available services.
     WebPki {},
 
-    /// Name of the SecretClass which will provide the ca cert.
-    /// Note that a SecretClass does not need to have a key but can also work with just a ca cert.
-    /// So if you got provided with a ca cert but don't have access to the key you can still use this method.
+    /// Name of the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) which will provide the CA certificate.
+    /// Note that a SecretClass does not need to have a key but can also work with just a CA certificate,
+    /// so if you got provided with a CA cert but don't have access to the key you can still use this method.
     SecretClass(String),
 }
diff --git a/src/commons/listener.rs b/src/commons/listener.rs
index ac69aa8a8..911b41a06 100644
--- a/src/commons/listener.rs
+++ b/src/commons/listener.rs
@@ -39,7 +39,9 @@ use k8s_openapi::api::core::v1::{
 #[cfg(doc)]
 use crate::builder::ListenerOperatorVolumeSourceBuilder;
 
-/// Defines a policy for how [`Listener`]s should be exposed.
+/// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed.
+/// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass)
+/// for more information.
 #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)]
 #[kube(
     group = "listeners.stackable.tech",
@@ -49,7 +51,8 @@ use crate::builder::ListenerOperatorVolumeSourceBuilder;
 #[serde(rename_all = "camelCase")]
 pub struct ListenerClassSpec {
     pub service_type: ServiceType,
-    /// Annotations that should be added to the [`Service`] object.
+
+    /// Annotations that should be added to the Service object.
     #[serde(default)]
     pub service_annotations: BTreeMap<String, String>,
 }
@@ -67,10 +70,13 @@ pub enum ServiceType {
 
 /// Exposes a set of pods to the outside world.
 ///
-/// Essentially a Stackable extension of a Kubernetes [`Service`]. Compared to [`Service`], [`Listener`] changes three things:
-/// 1. It uses a cluster-level policy object ([`ListenerClass`]) to define how exactly the exposure works
+/// Essentially a Stackable extension of a Kubernetes Service. Compared to a Service, a Listener changes three things:
+/// 1. It uses a cluster-level policy object (ListenerClass) to define how exactly the exposure works
 /// 2. It has a consistent API for reading back the exposed address(es) of the service
-/// 3. The [`Pod`] must mount a [`Volume`] referring to the `Listener`, which also allows us to control stickiness
+/// 3. The Pod must mount a Volume referring to the Listener, which also allows
+/// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling).
+///
+/// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener).
 #[derive(
     CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Default, PartialEq, Eq,
 )]
@@ -83,14 +89,17 @@ pub enum ServiceType {
 )]
 #[serde(rename_all = "camelCase")]
 pub struct ListenerSpec {
-    /// The name of the [`ListenerClass`].
+    /// The name of the [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass).
     pub class_name: Option<String>,
-    /// Extra labels that the [`Pod`]s must match in order to be exposed. They must _also_ still have a `Volume` referring to the listener.
+
+    /// Extra labels that the Pods must match in order to be exposed. They must _also_ still have a Volume referring to the Listener.
     #[serde(default)]
     pub extra_pod_selector_labels: BTreeMap<String, String>,
+
     /// Ports that should be exposed.
     pub ports: Option<Vec<ListenerPort>>,
-    /// Whether incoming traffic should also be directed to `Pod`s that are not `Ready`.
+
+    /// Whether incoming traffic should also be directed to Pods that are not `Ready`.
     #[schemars(default = "Self::default_publish_not_ready_addresses")]
     pub publish_not_ready_addresses: Option<bool>,
 }
@@ -106,7 +115,7 @@ impl ListenerSpec {
 pub struct ListenerPort {
     /// The name of the port.
     ///
-    /// The name of each port *must* be unique within a single [`Listener`].
+    /// The name of each port *must* be unique within a single Listener.
     pub name: String,
     /// The port number.
     pub port: i32,
@@ -114,26 +123,26 @@ pub struct ListenerPort {
     pub protocol: Option<String>,
 }
 
-/// Informs users about how to reach the [`Listener`].
+/// Informs users about how to reach the Listener.
 #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct ListenerStatus {
-    /// The backing Kubernetes [`Service`].
+    /// The backing Kubernetes Service.
     pub service_name: Option<String>,
-    /// All addresses that the [`Listener`] is currently reachable from.
+    /// All addresses that the Listener is currently reachable from.
     pub ingress_addresses: Option<Vec<ListenerIngress>>,
-    /// Port mappings for accessing the [`Listener`] on each [`Node`] that the [`Pod`]s are currently running on.
+    /// Port mappings for accessing the Listener on each Node that the Pods are currently running on.
     ///
-    /// This is only intended for internal use by listener-operator itself. This will be left unset if using a [`ListenerClass`] that does
-    /// not require [`Node`]-local access.
+    /// This is only intended for internal use by listener-operator itself. This will be left unset if using a ListenerClass that does
+    /// not require Node-local access.
     pub node_ports: Option<BTreeMap<String, i32>>,
 }
 
-/// One address that a [`Listener`] is accessible from.
+/// One address that a Listener is accessible from.
 #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct ListenerIngress {
-    /// The hostname or IP address to the [`Listener`].
+    /// The hostname or IP address to the Listener.
     pub address: String,
     /// The type of address (`Hostname` or `IP`).
     pub address_type: AddressType,
@@ -149,7 +158,7 @@ pub enum AddressType {
     Ip,
 }
 
-/// Informs users about [`Listener`]s that are bound by a given [`Pod`].
+/// Informs users about Listeners that are bound by a given Pod.
 ///
 /// This is not expected to be created or modified by users. It will be created by
 /// the Stackable Listener Operator when mounting the listener volume, and is always
@@ -166,22 +175,22 @@ pub enum AddressType {
 )]
 #[serde(rename_all = "camelCase")]
 pub struct PodListenersSpec {
-    /// All listeners currently bound by the [`Pod`].
+    /// All Listeners currently bound by the Pod.
     ///
-    /// Indexed by [`Volume`] name (not [`PersistentVolume`] or [`PersistentVolumeClaim`]).
+    /// Indexed by Volume name (not PersistentVolume or PersistentVolumeClaim).
     pub listeners: BTreeMap<String, PodListener>,
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct PodListener {
-    /// `Node` if this address only allows access to [`Pod`]s hosted on a specific Kubernetes [`Node`], otherwise `Cluster`.
+    /// `Node` if this address only allows access to Pods hosted on a specific Kubernetes Node, otherwise `Cluster`.
     pub scope: PodListenerScope,
-    /// Addresses allowing access to this [`Pod`].
+    /// Addresses allowing access to this Pod.
     ///
-    /// Compared to [`ListenerStatus::ingress_addresses`], this list is restricted to addresses that can access this [`Pod`].
+    /// Compared to `ingress_addresses` on the Listener status, this list is restricted to addresses that can access this Pod.
     ///
-    /// This field is intended to be equivalent to the files mounted into the listener volume.
+    /// This field is intended to be equivalent to the files mounted into the Listener volume.
     pub ingress_addresses: Option<Vec<ListenerIngress>>,
 }
 
diff --git a/src/commons/s3.rs b/src/commons/s3.rs
index e65bf65a8..7c7a80750 100644
--- a/src/commons/s3.rs
+++ b/src/commons/s3.rs
@@ -14,8 +14,8 @@ use crate::{
     error::{self, OperatorResult},
 };
 
-/// S3 bucket specification containing only the bucket name and an inlined or
-/// referenced connection specification.
+/// S3 bucket specification containing the bucket name and an inlined or referenced connection specification.
+/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3).
 #[derive(
     Clone, CustomResource, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize,
 )]
@@ -153,7 +153,8 @@ impl S3ConnectionDef {
     }
 }
 
-/// S3 connection definition as CRD.
+/// S3 connection definition as a resource.
+/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3).
 #[derive(
     CustomResource, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize,
 )]
@@ -189,7 +190,8 @@ pub struct S3ConnectionSpec {
     pub access_style: Option<S3AccessStyle>,
 
     /// If the S3 uses authentication you have to specify you S3 credentials.
-    /// In the most cases a SecretClass providing `accessKey` and `secretKey` is sufficient.
+    /// In the most cases a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass)
+    /// providing `accessKey` and `secretKey` is sufficient.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub credentials: Option<SecretClassVolume>,
 
diff --git a/src/commons/secret_class.rs b/src/commons/secret_class.rs
index 93ee1ed01..b8fc56afc 100644
--- a/src/commons/secret_class.rs
+++ b/src/commons/secret_class.rs
@@ -6,9 +6,10 @@ use serde::{Deserialize, Serialize};
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct SecretClassVolume {
-    /// [SecretClass](https://docs.stackable.tech/secret-operator/secretclass.html) containing the LDAP bind credentials
+    /// [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) containing the LDAP bind credentials.
     pub secret_class: String,
-    /// [Scope](https://docs.stackable.tech/secret-operator/scope.html) of the [SecretClass](https://docs.stackable.tech/secret-operator/secretclass.html)
+    /// [Scope](DOCS_BASE_URL_PLACEHOLDER/secret-operator/scope) of the
+    /// [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass).
     pub scope: Option<SecretClassVolumeScope>,
 }
 
@@ -42,10 +43,18 @@ impl SecretClassVolume {
 #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct SecretClassVolumeScope {
+    /// The pod scope is resolved to the name of the Kubernetes Pod.
+    /// This allows the secret to differentiate between StatefulSet replicas.
     #[serde(default)]
     pub pod: bool,
+
+    /// The node scope is resolved to the name of the Kubernetes Node object that the Pod is running on.
+    /// This will typically be the DNS name of the node.
     #[serde(default)]
     pub node: bool,
+
+    /// The service scope allows Pod objects to specify custom scopes.
+    /// This should typically correspond to Service objects that the Pod participates in.
     #[serde(default)]
     pub services: Vec<String>,
 }
diff --git a/src/role_utils.rs b/src/role_utils.rs
index 7f83a9fb0..f9072f7eb 100644
--- a/src/role_utils.rs
+++ b/src/role_utils.rs
@@ -147,11 +147,13 @@ fn config_schema_default() -> serde_json::Value {
     serde_json::json!({})
 }
 
-/// This struct represents a role - e.g. HDFS datanodes or Trino workers. It has a [`HashMap`] containing
+/// This struct represents a role - e.g. HDFS datanodes or Trino workers. It has a key-value-map containing
 /// all the roleGroups that are part of this role. Additionally, there is a `config`, which is configurable
 /// at the role *and* roleGroup level. Everything at roleGroup level is merged on top of what is configured
-/// on role level using the [`Merge`] trait. There is also a second form of config, which can only be configured
+/// on role level. There is also a second form of config, which can only be configured
 /// at role level, the `roleConfig`.
+/// You can learn more about this in the
+/// [Roles and role group concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/roles-and-role-groups).
 //
 // Everything below is only a "normal" comment, not rustdoc - so we don't bloat the CRD documentation
 // with technical (Rust) details.