align cpp sdk with rust, add docs
diff --git a/foreign/cpp/include/iggy.hpp b/foreign/cpp/include/iggy.hpp
index f615ee3..6a4b1e2 100644
--- a/foreign/cpp/include/iggy.hpp
+++ b/foreign/cpp/include/iggy.hpp
@@ -101,7 +101,16 @@
  */
 class IggyException : public std::runtime_error {
   public:
+    /**
+     * @brief Creates an Iggy exception from a null-terminated message.
+     * @param message Error description.
+     */
     explicit IggyException(const char *message) : std::runtime_error(message) {}
+
+    /**
+     * @brief Creates an Iggy exception from an owning string.
+     * @param message Error description.
+     */
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
@@ -159,7 +168,10 @@
  */
 class Identifier final {
   public:
+    /** @brief Maximum encoded length of a name-based identifier. */
     static constexpr std::size_t kMaxIdentifierLength = 255;
+
+    /** @brief Selects the representation stored by an Identifier. */
     enum class Kind : std::uint8_t { Numeric, String };
 
     /**
@@ -207,69 +219,185 @@
     std::variant<std::uint32_t, std::string> value_;
 };
 
+/**
+ * @brief Controls whether a user may authenticate.
+ */
 enum class UserStatus : std::uint8_t {
-    Active   = 1,
-    Inactive = 2,
+    Active   = 1,  ///< The user may authenticate.
+    Inactive = 2,  ///< Authentication for the user is rejected.
 };
 
+/**
+ * @brief Cluster-wide permissions assigned to a user.
+ *
+ * Global grants apply without naming individual streams or topics. Management
+ * grants include the corresponding read grants. Stream and topic grants form a
+ * hierarchy: managing streams includes managing topics, reading streams
+ * includes reading topics, and reading topics includes polling messages and
+ * managing consumer groups. Managing streams or topics also authorizes sending
+ * messages; SendMessages() can grant sending without management permission.
+ *
+ * A default-constructed value has every flag disabled. The setters record the
+ * supplied flags without expanding implied grants; the server applies the
+ * hierarchy when authorizing a request.
+ */
 class GlobalPermissions final {
   public:
+    /**
+     * @brief Returns the configured cluster-management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageServers() const noexcept { return manage_servers_; }
+
+    /**
+     * @brief Returns the configured server-information read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadServers() const noexcept { return read_servers_; }
+
+    /**
+     * @brief Returns the configured user-management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageUsers() const noexcept { return manage_users_; }
+
+    /**
+     * @brief Returns the configured user-information read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadUsers() const noexcept { return read_users_; }
+
+    /**
+     * @brief Returns the configured all-stream management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageStreams() const noexcept { return manage_streams_; }
+
+    /**
+     * @brief Returns the configured all-stream read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadStreams() const noexcept { return read_streams_; }
+
+    /**
+     * @brief Returns the configured all-topic management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageTopics() const noexcept { return manage_topics_; }
+
+    /**
+     * @brief Returns the configured all-topic read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadTopics() const noexcept { return read_topics_; }
+
+    /**
+     * @brief Returns the configured all-topic message-polling flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool PollMessages() const noexcept { return poll_messages_; }
+
+    /**
+     * @brief Returns the configured all-topic message-sending flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool SendMessages() const noexcept { return send_messages_; }
 
+    /**
+     * @brief Enables or disables cluster-management permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetManageServers(bool enabled) {
         manage_servers_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables permission to read server information.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetReadServers(bool enabled) {
         read_servers_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables user-management permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetManageUsers(bool enabled) {
         manage_users_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables permission to read user information.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetReadUsers(bool enabled) {
         read_users_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables management permission for every stream.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetManageStreams(bool enabled) {
         manage_streams_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables read permission for every stream.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetReadStreams(bool enabled) {
         read_streams_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables management permission for every topic.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetManageTopics(bool enabled) {
         manage_topics_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables read permission for every topic.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetReadTopics(bool enabled) {
         read_topics_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables polling permission for every topic.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetPollMessages(bool enabled) {
         poll_messages_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables sending permission for every topic.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     GlobalPermissions &SetSendMessages(bool enabled) {
         send_messages_ = enabled;
         return *this;
@@ -293,28 +421,78 @@
     bool send_messages_{};
 };
 
+/**
+ * @brief Permissions extending a user's access to one topic.
+ *
+ * These flags grant access in addition to enclosing stream and global grants;
+ * a disabled flag does not revoke access granted at a broader scope. Managing
+ * a topic includes reading it. Reading a topic includes polling messages and
+ * managing its consumer groups. Managing a topic also authorizes sending;
+ * SetSendMessages() can grant sending without management permission.
+ *
+ * A default-constructed value has every flag disabled.
+ */
 class TopicPermissions final {
   public:
+    /**
+     * @brief Returns the configured topic-management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageTopic() const noexcept { return manage_topic_; }
+
+    /**
+     * @brief Returns the configured topic-read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadTopic() const noexcept { return read_topic_; }
+
+    /**
+     * @brief Returns the configured message-polling flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool PollMessages() const noexcept { return poll_messages_; }
+
+    /**
+     * @brief Returns the configured message-sending flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool SendMessages() const noexcept { return send_messages_; }
 
+    /**
+     * @brief Enables or disables topic-management permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     TopicPermissions &SetManageTopic(bool enabled) {
         manage_topic_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables topic-read permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     TopicPermissions &SetReadTopic(bool enabled) {
         read_topic_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables message-polling permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     TopicPermissions &SetPollMessages(bool enabled) {
         poll_messages_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables message-sending permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     TopicPermissions &SetSendMessages(bool enabled) {
         send_messages_ = enabled;
         return *this;
@@ -332,46 +510,127 @@
     bool send_messages_{};
 };
 
+/**
+ * @brief Permissions extending a user's access to one stream.
+ *
+ * Stream grants apply to the stream identified by the containing Permissions
+ * map. Topic-specific grants are keyed by numeric topic ID. These flags extend
+ * broader grants and cannot revoke permissions granted globally. Managing a
+ * stream includes reading it and managing its topics; reading a stream includes
+ * reading its topics. Managing a stream or its topics also authorizes sending;
+ * SetSendMessages() can grant sending without management permission.
+ *
+ * A default-constructed value has every flag disabled and no topic entries.
+ */
 class StreamPermissions final {
   public:
+    /**
+     * @brief Returns the configured stream-management flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageStream() const noexcept { return manage_stream_; }
+
+    /**
+     * @brief Returns the configured stream-read flag.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadStream() const noexcept { return read_stream_; }
+
+    /**
+     * @brief Returns the configured management flag for all stream topics.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ManageTopics() const noexcept { return manage_topics_; }
+
+    /**
+     * @brief Returns the configured read flag for all stream topics.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool ReadTopics() const noexcept { return read_topics_; }
+
+    /**
+     * @brief Returns the configured polling flag for all stream topics.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool PollMessages() const noexcept { return poll_messages_; }
+
+    /**
+     * @brief Returns the configured sending flag for all stream topics.
+     * @return Configured flag value.
+     */
     [[nodiscard]] bool SendMessages() const noexcept { return send_messages_; }
+
+    /**
+     * @brief Returns topic-specific grants keyed by numeric topic ID.
+     * @return Map owned by this value.
+     */
     [[nodiscard]] const std::map<std::uint32_t, TopicPermissions> &Topics() const noexcept { return topics_; }
 
+    /**
+     * @brief Enables or disables stream-management permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetManageStream(bool enabled) {
         manage_stream_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables stream-read permission.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetReadStream(bool enabled) {
         read_stream_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables management permission for all stream topics.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetManageTopics(bool enabled) {
         manage_topics_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables read permission for all stream topics.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetReadTopics(bool enabled) {
         read_topics_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables polling permission for all stream topics.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetPollMessages(bool enabled) {
         poll_messages_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Enables or disables sending permission for all stream topics.
+     * @param enabled Requested flag value.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetSendMessages(bool enabled) {
         send_messages_ = enabled;
         return *this;
     }
 
+    /**
+     * @brief Replaces the topic-specific permission map.
+     * @param topics Grants keyed by numeric topic ID.
+     * @return Reference to this permissions object.
+     */
     StreamPermissions &SetTopics(std::map<std::uint32_t, TopicPermissions> topics) {
         topics_ = std::move(topics);
         return *this;
@@ -392,16 +651,46 @@
     std::map<std::uint32_t, TopicPermissions> topics_;
 };
 
+/**
+ * @brief Complete permission assignment for a user.
+ *
+ * Global permissions apply cluster-wide. Stream entries are keyed by numeric
+ * stream ID and add narrower grants, including optional topic-specific grants.
+ * Narrower scopes extend broader scopes and do not deny an inherited grant.
+ *
+ * A default-constructed value contains no grants. Passing such a value to
+ * CreateUser() assigns an explicit but empty permission set; passing
+ * `std::nullopt` assigns no permission object.
+ */
 class Permissions final {
   public:
+    /**
+     * @brief Returns the cluster-wide grants.
+     * @return Global permissions owned by this value.
+     */
     [[nodiscard]] const GlobalPermissions &Global() const noexcept { return global_; }
+
+    /**
+     * @brief Returns stream-specific grants keyed by numeric stream ID.
+     * @return Map owned by this value.
+     */
     [[nodiscard]] const std::map<std::uint32_t, StreamPermissions> &Streams() const noexcept { return streams_; }
 
+    /**
+     * @brief Replaces the cluster-wide grants.
+     * @param global New global permissions.
+     * @return Reference to this permissions object.
+     */
     Permissions &SetGlobal(GlobalPermissions global) {
         global_ = global;
         return *this;
     }
 
+    /**
+     * @brief Replaces the stream-specific grants.
+     * @param streams Grants keyed by numeric stream ID.
+     * @return Reference to this permissions object.
+     */
     Permissions &SetStreams(std::map<std::uint32_t, StreamPermissions> streams) {
         streams_ = std::move(streams);
         return *this;
@@ -418,58 +707,6 @@
     std::map<std::uint32_t, StreamPermissions> streams_;
 };
 
-class UserInfo final {
-  public:
-    [[nodiscard]] std::uint32_t Id() const noexcept { return id_; }
-    [[nodiscard]] std::uint64_t CreatedAt() const noexcept { return created_at_; }
-    [[nodiscard]] UserStatus Status() const noexcept { return status_; }
-    [[nodiscard]] const std::string &Username() const noexcept { return username_; }
-
-  private:
-    UserInfo(std::uint32_t id, std::uint64_t created_at, UserStatus status, std::string username)
-        : id_(id), created_at_(created_at), status_(status), username_(std::move(username)) {}
-
-    static UserInfo FromFfi(ffi::UserInfo user);
-
-    friend class IggyBlockingClient;
-
-    std::uint32_t id_;
-    std::uint64_t created_at_;
-    UserStatus status_;
-    std::string username_;
-};
-
-class UserInfoDetails final {
-  public:
-    [[nodiscard]] std::uint32_t Id() const noexcept { return id_; }
-    [[nodiscard]] std::uint64_t CreatedAt() const noexcept { return created_at_; }
-    [[nodiscard]] UserStatus Status() const noexcept { return status_; }
-    [[nodiscard]] const std::string &Username() const noexcept { return username_; }
-    [[nodiscard]] const std::optional<::iggy::Permissions> &Permissions() const noexcept { return permissions_; }
-
-  private:
-    UserInfoDetails(std::uint32_t id,
-                    std::uint64_t created_at,
-                    UserStatus status,
-                    std::string username,
-                    std::optional<::iggy::Permissions> permissions)
-        : id_(id),
-          created_at_(created_at),
-          status_(status),
-          username_(std::move(username)),
-          permissions_(std::move(permissions)) {}
-
-    static UserInfoDetails FromFfi(ffi::UserInfoDetails user);
-
-    friend class IggyBlockingClient;
-
-    std::uint32_t id_;
-    std::uint64_t created_at_;
-    UserStatus status_;
-    std::string username_;
-    std::optional<::iggy::Permissions> permissions_;
-};
-
 /**
  * @brief Identifies the owner of a stored consumer offset.
  *
@@ -479,6 +716,7 @@
  */
 class Consumer final {
   public:
+    /** @brief Selects an individual consumer or consumer-group identity. */
     enum class Kind : std::uint8_t { Single, Group };
 
     /**
@@ -882,12 +1120,141 @@
     friend class TopicDetails;
     friend class Stream;
     friend class StreamDetails;
+    friend class UserInfo;
+    friend class UserInfoDetails;
 
     std::map<std::string, HeaderField> explicit_;
     std::map<std::string, HeaderField> derived_;
 };
 
 /**
+ * @brief Snapshot of basic user metadata.
+ *
+ * GetUsers() returns one value for each user visible to the caller. This
+ * summary omits permissions. User IDs remain assigned to the same user until
+ * that user is deleted. CreatedAt() is expressed in microseconds since the
+ * Unix epoch.
+ */
+class UserInfo final {
+  public:
+    /**
+     * @brief Returns the server-assigned numeric user ID.
+     * @return Numeric user ID.
+     */
+    [[nodiscard]] std::uint32_t Id() const noexcept { return id_; }
+
+    /**
+     * @brief Returns the creation timestamp.
+     * @return Timestamp in microseconds since the Unix epoch.
+     */
+    [[nodiscard]] std::uint64_t CreatedAt() const noexcept { return created_at_; }
+
+    /**
+     * @brief Returns whether the user is active or inactive.
+     * @return Current user status observed for this request.
+     */
+    [[nodiscard]] UserStatus Status() const noexcept { return status_; }
+
+    /**
+     * @brief Returns the unique user name.
+     * @return Name owned by this value.
+     */
+    [[nodiscard]] const std::string &Username() const noexcept { return username_; }
+
+    [[nodiscard]] const ResourceOptions &Options() const noexcept { return options_; }
+
+  private:
+    UserInfo(std::uint32_t id,
+             std::uint64_t created_at,
+             UserStatus status,
+             std::string username,
+             ResourceOptions options)
+        : id_(id),
+          created_at_(created_at),
+          status_(status),
+          username_(std::move(username)),
+          options_(std::move(options)) {}
+
+    static UserInfo FromFfi(ffi::UserInfo user);
+
+    friend class IggyBlockingClient;
+
+    std::uint32_t id_;
+    std::uint64_t created_at_;
+    UserStatus status_;
+    std::string username_;
+    ResourceOptions options_;
+};
+
+/**
+ * @brief Snapshot of user metadata and assigned permissions.
+ *
+ * GetUser() and CreateUser() return this detailed form. Permissions() is empty
+ * when the user has no permission object. An engaged Permissions value can
+ * still contain no enabled grants.
+ */
+class UserInfoDetails final {
+  public:
+    /**
+     * @brief Returns the server-assigned numeric user ID.
+     * @return Numeric user ID.
+     */
+    [[nodiscard]] std::uint32_t Id() const noexcept { return id_; }
+
+    /**
+     * @brief Returns the creation timestamp.
+     * @return Timestamp in microseconds since the Unix epoch.
+     */
+    [[nodiscard]] std::uint64_t CreatedAt() const noexcept { return created_at_; }
+
+    /**
+     * @brief Returns whether the user is active or inactive.
+     * @return Current user status observed for this request.
+     */
+    [[nodiscard]] UserStatus Status() const noexcept { return status_; }
+
+    /**
+     * @brief Returns the unique user name.
+     * @return Name owned by this value.
+     */
+    [[nodiscard]] const std::string &Username() const noexcept { return username_; }
+
+    /**
+     * @brief Returns the user's explicit permission assignment.
+     * @return Empty when no permission object is assigned; otherwise the
+     *         permissions owned by this value.
+     */
+    [[nodiscard]] const std::optional<::iggy::Permissions> &Permissions() const noexcept { return permissions_; }
+
+    [[nodiscard]] const ResourceOptions &Options() const noexcept { return options_; }
+
+  private:
+    UserInfoDetails(std::uint32_t id,
+                    std::uint64_t created_at,
+                    UserStatus status,
+                    std::string username,
+                    std::optional<::iggy::Permissions> permissions,
+                    ResourceOptions options)
+        : id_(id),
+          created_at_(created_at),
+          status_(status),
+          username_(std::move(username)),
+          permissions_(std::move(permissions)),
+          options_(std::move(options)) {}
+
+    static UserInfoDetails FromFfi(ffi::UserInfoDetails user);
+
+    friend class IggyBlockingClient;
+
+    std::uint32_t id_;
+    std::uint64_t created_at_;
+    UserStatus status_;
+    std::string username_;
+    std::optional<::iggy::Permissions> permissions_;
+    ResourceOptions options_;
+};
+
+/**
  * @brief Snapshot of one topic's metadata and aggregate statistics.
  *
  * GetStream() returns one of these values for each observed topic. It owns its
@@ -1546,10 +1913,31 @@
     std::vector<ConsumerGroupMember> members_;
 };
 
+/**
+ * @brief Identifies one consumer-group membership of a connected client.
+ *
+ * ClientInfoDetails returns these numeric identifiers for each membership
+ * observed by the server. The membership can change immediately after the
+ * client information is retrieved.
+ */
 class ConsumerGroupInfo final {
   public:
+    /**
+     * @brief Returns the numeric ID of the member group's stream.
+     * @return Numeric stream ID.
+     */
     [[nodiscard]] std::uint32_t StreamId() const noexcept { return stream_id_; }
+
+    /**
+     * @brief Returns the numeric ID of the member group's topic.
+     * @return Numeric topic ID.
+     */
     [[nodiscard]] std::uint32_t TopicId() const noexcept { return topic_id_; }
+
+    /**
+     * @brief Returns the numeric consumer group ID.
+     * @return Numeric consumer group ID.
+     */
     [[nodiscard]] std::uint32_t GroupId() const noexcept { return group_id_; }
 
   private:
@@ -1565,12 +1953,45 @@
     std::uint32_t group_id_;
 };
 
+/**
+ * @brief Snapshot summary of a client connection known to the server.
+ *
+ * GetClients() returns one summary for each connection observed by the server.
+ * A client is a transport connection, not an Iggy user. Connections can close,
+ * authenticate, or change consumer-group membership immediately after the
+ * request completes.
+ */
 class ClientInfo final {
   public:
+    /**
+     * @brief Returns the server-assigned connection ID.
+     * @return Numeric client ID accepted by GetClient() while the connection
+     *         remains known to the server.
+     */
     [[nodiscard]] std::uint32_t ClientId() const noexcept { return client_id_; }
+
+    /**
+     * @brief Returns the authenticated user ID for this connection.
+     * @return Empty when the client has not authenticated.
+     */
     [[nodiscard]] const std::optional<std::uint32_t> &UserId() const noexcept { return user_id_; }
+
+    /**
+     * @brief Returns the remote address reported by the server.
+     * @return Address owned by this value.
+     */
     [[nodiscard]] const std::string &Address() const noexcept { return address_; }
+
+    /**
+     * @brief Returns the transport name reported by the server.
+     * @return Transport name owned by this value.
+     */
     [[nodiscard]] const std::string &Transport() const noexcept { return transport_; }
+
+    /**
+     * @brief Returns the number of consumer groups joined by this client.
+     * @return Membership count observed for this request.
+     */
     [[nodiscard]] std::uint32_t ConsumerGroupsCount() const noexcept { return consumer_groups_count_; }
 
   private:
@@ -1596,13 +2017,48 @@
     std::uint32_t consumer_groups_count_;
 };
 
+/**
+ * @brief Snapshot of a client connection and its consumer-group memberships.
+ *
+ * GetMe() and GetClient() return this detailed form. The connection state and
+ * memberships are not live and can change immediately after retrieval.
+ */
 class ClientInfoDetails final {
   public:
+    /**
+     * @brief Returns the server-assigned connection ID.
+     * @return Numeric client ID.
+     */
     [[nodiscard]] std::uint32_t ClientId() const noexcept { return client_id_; }
+
+    /**
+     * @brief Returns the authenticated user ID for this connection.
+     * @return Empty when the client has not authenticated.
+     */
     [[nodiscard]] const std::optional<std::uint32_t> &UserId() const noexcept { return user_id_; }
+
+    /**
+     * @brief Returns the remote address reported by the server.
+     * @return Address owned by this value.
+     */
     [[nodiscard]] const std::string &Address() const noexcept { return address_; }
+
+    /**
+     * @brief Returns the transport name reported by the server.
+     * @return Transport name owned by this value.
+     */
     [[nodiscard]] const std::string &Transport() const noexcept { return transport_; }
+
+    /**
+     * @brief Returns the server-reported consumer-group membership count.
+     * @return Membership count observed for this request.
+     */
     [[nodiscard]] std::uint32_t ConsumerGroupsCount() const noexcept { return consumer_groups_count_; }
+
+    /**
+     * @brief Returns the observed consumer-group memberships.
+     * @return Membership identifiers owned by this value.
+     */
     [[nodiscard]] const std::vector<ConsumerGroupInfo> &ConsumerGroups() const noexcept { return consumer_groups_; }
 
   private:
@@ -1631,13 +2087,49 @@
     std::vector<ConsumerGroupInfo> consumer_groups_;
 };
 
+/**
+ * @brief Cache counters for one stream, topic, and partition.
+ *
+ * Stats::CacheMetrics() contains these entries when the server implementation
+ * reports partition cache metrics. The current VSR server returns an empty
+ * cache-metrics collection.
+ */
 class CacheMetricEntry final {
   public:
+    /**
+     * @brief Returns the numeric stream ID for this cache entry.
+     * @return Numeric stream ID.
+     */
     [[nodiscard]] std::uint32_t StreamId() const noexcept { return stream_id_; }
+
+    /**
+     * @brief Returns the numeric topic ID for this cache entry.
+     * @return Numeric topic ID.
+     */
     [[nodiscard]] std::uint32_t TopicId() const noexcept { return topic_id_; }
+
+    /**
+     * @brief Returns the numeric partition ID for this cache entry.
+     * @return Numeric partition ID.
+     */
     [[nodiscard]] std::uint32_t PartitionId() const noexcept { return partition_id_; }
+
+    /**
+     * @brief Returns the cumulative number of cache hits reported by the server.
+     * @return Cache hit count.
+     */
     [[nodiscard]] std::uint64_t Hits() const noexcept { return hits_; }
+
+    /**
+     * @brief Returns the cumulative number of cache misses reported by the server.
+     * @return Cache miss count.
+     */
     [[nodiscard]] std::uint64_t Misses() const noexcept { return misses_; }
+
+    /**
+     * @brief Returns the server-reported ratio of hits to total cache lookups.
+     * @return Cache hit ratio.
+     */
     [[nodiscard]] float HitRatio() const noexcept { return hit_ratio_; }
 
   private:
@@ -1666,35 +2158,187 @@
     float hit_ratio_;
 };
 
+/**
+ * @brief Snapshot of server process, storage, and resource statistics.
+ *
+ * GetStats() returns process and host measurements from the serving server,
+ * together with metadata totals observed for one request. Values are not a
+ * live or transactional view. CPU measurements depend on the server's sampling
+ * history, and the first sample on a serving thread can report zero. Memory
+ * totals honor an effective cgroup limit when one applies. Disk-space values
+ * describe the volume containing the configured data directory and can be zero
+ * when the server cannot probe that volume.
+ */
 class Stats final {
   public:
+    /**
+     * @brief Returns the operating-system process ID of the server.
+     * @return Numeric process ID.
+     */
     [[nodiscard]] std::uint32_t ProcessId() const noexcept { return process_id_; }
+
+    /**
+     * @brief Returns the server process CPU usage.
+     * @return Process CPU usage as a percentage.
+     */
     [[nodiscard]] float CpuUsage() const noexcept { return cpu_usage_; }
+
+    /**
+     * @brief Returns total CPU usage for the available CPU set.
+     * @return Total CPU usage as a percentage.
+     */
     [[nodiscard]] float TotalCpuUsage() const noexcept { return total_cpu_usage_; }
+
+    /**
+     * @brief Returns server process memory usage.
+     * @return Process memory usage in bytes.
+     */
     [[nodiscard]] std::uint64_t MemoryUsage() const noexcept { return memory_usage_; }
+
+    /**
+     * @brief Returns total host or effective cgroup memory.
+     * @return Total memory in bytes.
+     */
     [[nodiscard]] std::uint64_t TotalMemory() const noexcept { return total_memory_; }
+
+    /**
+     * @brief Returns available host or effective cgroup memory.
+     * @return Available memory in bytes.
+     */
     [[nodiscard]] std::uint64_t AvailableMemory() const noexcept { return available_memory_; }
+
+    /**
+     * @brief Returns server process uptime.
+     * @return Process uptime in microseconds.
+     */
     [[nodiscard]] std::uint64_t RunTimeMicros() const noexcept { return run_time_micros_; }
+
+    /**
+     * @brief Returns the server process start time.
+     * @return Timestamp in microseconds since the Unix epoch.
+     */
     [[nodiscard]] std::uint64_t StartTimeEpochMicros() const noexcept { return start_time_epoch_micros_; }
+
+    /**
+     * @brief Returns the server process read-byte count.
+     * @return Number of bytes read by the process.
+     */
     [[nodiscard]] std::uint64_t ReadBytes() const noexcept { return read_bytes_; }
+
+    /**
+     * @brief Returns the server process written-byte count.
+     * @return Number of bytes written by the process.
+     */
     [[nodiscard]] std::uint64_t WrittenBytes() const noexcept { return written_bytes_; }
+
+    /**
+     * @brief Returns the aggregate retained message size.
+     * @return Retained message size in bytes.
+     */
     [[nodiscard]] std::uint64_t MessagesSizeBytes() const noexcept { return messages_size_bytes_; }
+
+    /**
+     * @brief Returns the observed number of streams.
+     * @return Stream count.
+     */
     [[nodiscard]] std::uint32_t StreamsCount() const noexcept { return streams_count_; }
+
+    /**
+     * @brief Returns the observed number of topics.
+     * @return Topic count.
+     */
     [[nodiscard]] std::uint32_t TopicsCount() const noexcept { return topics_count_; }
+
+    /**
+     * @brief Returns the observed number of partitions.
+     * @return Partition count.
+     */
     [[nodiscard]] std::uint32_t PartitionsCount() const noexcept { return partitions_count_; }
+
+    /**
+     * @brief Returns the observed number of partition segments.
+     * @return Segment count.
+     */
     [[nodiscard]] std::uint32_t SegmentsCount() const noexcept { return segments_count_; }
+
+    /**
+     * @brief Returns the observed number of retained messages.
+     * @return Message count.
+     */
     [[nodiscard]] std::uint64_t MessagesCount() const noexcept { return messages_count_; }
+
+    /**
+     * @brief Returns the number of client connections observed by the server.
+     * @return Client connection count.
+     */
     [[nodiscard]] std::uint32_t ClientsCount() const noexcept { return clients_count_; }
+
+    /**
+     * @brief Returns the observed number of consumer groups.
+     * @return Consumer group count.
+     */
     [[nodiscard]] std::uint32_t ConsumerGroupsCount() const noexcept { return consumer_groups_count_; }
+
+    /**
+     * @brief Returns the server host name.
+     * @return Host name owned by this value.
+     */
     [[nodiscard]] const std::string &Hostname() const noexcept { return hostname_; }
+
+    /**
+     * @brief Returns the server operating-system name.
+     * @return Operating-system name owned by this value.
+     */
     [[nodiscard]] const std::string &OsName() const noexcept { return os_name_; }
+
+    /**
+     * @brief Returns the server operating-system version.
+     * @return Operating-system version owned by this value.
+     */
     [[nodiscard]] const std::string &OsVersion() const noexcept { return os_version_; }
+
+    /**
+     * @brief Returns the server kernel version.
+     * @return Kernel version owned by this value.
+     */
     [[nodiscard]] const std::string &KernelVersion() const noexcept { return kernel_version_; }
+
+    /**
+     * @brief Returns the human-readable Iggy server version.
+     * @return Version string owned by this value.
+     */
     [[nodiscard]] const std::string &IggyServerVersion() const noexcept { return iggy_server_version_; }
+
+    /**
+     * @brief Returns the numeric semantic version when reported by the server.
+     * @return `major * 1,000,000 + minor * 1,000 + patch`, or `std::nullopt`
+     *         when the server does not report a numeric version.
+     */
     [[nodiscard]] const std::optional<std::uint32_t> &ServerSemver() const noexcept { return server_semver_; }
+
+    /**
+     * @brief Returns partition cache metrics reported by the server.
+     * @return Entries owned by this value. The current VSR server returns an
+     *         empty collection.
+     */
     [[nodiscard]] const std::vector<CacheMetricEntry> &CacheMetrics() const noexcept { return cache_metrics_; }
+
+    /**
+     * @brief Returns the number of threads in the server process.
+     * @return Process thread count.
+     */
     [[nodiscard]] std::uint32_t ThreadsCount() const noexcept { return threads_count_; }
+
+    /**
+     * @brief Returns free space on the server data-directory volume.
+     * @return Free space in bytes, or zero when the probe is unavailable.
+     */
     [[nodiscard]] std::uint64_t FreeDiskSpace() const noexcept { return free_disk_space_; }
+
+    /**
+     * @brief Returns total space on the server data-directory volume.
+     * @return Total space in bytes, or zero when the probe is unavailable.
+     */
     [[nodiscard]] std::uint64_t TotalDiskSpace() const noexcept { return total_disk_space_; }
 
   private:
@@ -1988,8 +2632,25 @@
     std::uint64_t expiry_value_;
 };
 
-enum class Durability : std::uint8_t { Replicated, Persisted };
+/**
+ * @brief Storage guarantee required before an operation reports completion.
+ *
+ * Both policies persist data through the replicated journal. Replicated waits
+ * for quorum commit without an additional stable-storage barrier. Persisted
+ * also requires recoverable stable-storage copies on the quorum. Topic message
+ * durability and consumer-offset durability are configured independently.
+ */
+enum class Durability : std::uint8_t {
+    Replicated,  ///< Wait for quorum commit.
+    Persisted,   ///< Wait for quorum commit backed by stable storage.
+};
 
+/**
+ * @brief Returns the protocol option name for a durability policy.
+ * @param durability Durability policy to encode.
+ * @return `replicated` or `persisted`.
+ * @throws std::invalid_argument if @p durability is not a declared enumerator.
+ */
 constexpr std::string_view to_string(const Durability durability) {
     switch (durability) {
         case Durability::Replicated:
@@ -2010,6 +2671,7 @@
  */
 class TopicCreateOptions final {
   public:
+    /** @brief Creates options with every setting left to the server default. */
     TopicCreateOptions() = default;
 
     /**
@@ -2279,6 +2941,7 @@
  */
 class TopicUpdateOptions final {
   public:
+    /** @brief Creates an update that leaves every topic setting unchanged. */
     TopicUpdateOptions() = default;
 
     /**
@@ -2401,6 +3064,7 @@
  */
 class StreamUpdateOptions final {
   public:
+    /** @brief Creates an update with no requested stream settings. */
     StreamUpdateOptions() = default;
 
     /**
@@ -2445,6 +3109,61 @@
 };
 
 /**
+ * @brief Options for updating a user.
+ *
+ * Use this class to supply user settings to UpdateUser(). Updating a user
+ * patches only the supplied settings; omitted settings remain unchanged.
+ * Currently, Iggy does not support updating user settings, so the server
+ * rejects every supplied setting. The raw entries are retained for
+ * compatibility with future server versions that add mutable user settings.
+ */
+class UserUpdateOptions final {
+  public:
+    /** @brief Creates an update with no requested user settings. */
+    UserUpdateOptions() = default;
+
+    /**
+     * @brief Returns the requested user settings as key-value pairs.
+     * @return Ordered map of setting names and values.
+     * @note The server currently rejects all user settings.
+     */
+    [[nodiscard]] const std::map<std::string, std::string> &RawEntries() const noexcept { return raw_; }
+
+    /**
+     * @brief Adds or replaces requested user settings.
+     * @param entries Setting names and values to add.
+     * @return Reference to this options object.
+     * @note The server currently rejects all user settings.
+     */
+    UserUpdateOptions &SetRawEntries(const std::map<std::string, std::string> &entries) {
+        for (const auto &entry : entries) {
+            raw_.insert_or_assign(entry.first, entry.second);
+        }
+        return *this;
+    }
+    /**
+     * @brief Adds or replaces requested user settings.
+     * @param entries Setting names and values to move into this options object.
+     * @return Reference to this options object.
+     * @see SetRawEntries(const std::map<std::string, std::string>&)
+     * @note The server currently rejects all user settings.
+     */
+    UserUpdateOptions &SetRawEntries(std::map<std::string, std::string> &&entries) {
+        while (!entries.empty()) {
+            auto node = entries.extract(entries.begin());
+            raw_.erase(node.key());
+            raw_.insert(std::move(node));
+        }
+        return *this;
+    }
+
+  private:
+    std::map<std::string, std::string> raw_;
+
+    friend class IggyBlockingClient;
+};
+
+/**
  * @brief Starting position for polling messages.
  *
  * @note The strategy kind and value are passed across the Rust FFI as a pair.
@@ -2722,16 +3441,108 @@
      */
     void Logout();
 
+    /**
+     * @brief Retrieves one user by numeric ID or name.
+     *
+     * An authenticated user may retrieve its own account without the global
+     * read-users grant. Reading another account requires read-users or
+     * manage-users permission. The result is a snapshot and includes the
+     * target user's optional permission assignment.
+     *
+     * @param user User to retrieve, addressed by numeric ID or name.
+     * @return Details for the requested user.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the identifier is invalid; the user does not exist; the caller
+     *         lacks permission; or the request fails.
+     */
     UserInfoDetails GetUser(const Identifier &user);
+
+    /**
+     * @brief Lists user summaries visible to the authenticated caller.
+     *
+     * The summaries omit permissions. Use GetUser() to retrieve one user's
+     * permission assignment.
+     *
+     * @return User summaries observed by the server for this request.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the caller lacks read-users or manage-users permission; or the
+     *         request fails.
+     */
     std::vector<UserInfo> GetUsers();
+
+    /**
+     * @brief Creates a user account.
+     *
+     * User names must contain between 3 and 50 bytes and be unique. Passwords
+     * must contain between 3 and 100 bytes. An inactive account is created but
+     * cannot authenticate. Passing `std::nullopt` assigns no permission object;
+     * passing a default-constructed Permissions assigns an explicit permission
+     * object with no enabled grants.
+     *
+     * A failed or unknown transport outcome can leave the user created. Query
+     * the account by name before retrying this request.
+     *
+     * @param username Unique user name.
+     * @param password Initial user password.
+     * @param status Initial authentication status.
+     * @param permissions Optional permission assignment.
+     * @return Details of the newly created user.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the name, password, status, or permissions are invalid; the name
+     *         is already in use; the caller lacks manage-users permission; or
+     *         the request fails.
+     */
     UserInfoDetails CreateUser(std::string username,
                                std::string password,
                                UserStatus status,
                                const std::optional<Permissions> &permissions = std::nullopt);
+
+    /**
+     * @brief Deletes a user account.
+     *
+     * The root user cannot be deleted. Deleting another user also removes its
+     * permission assignment and personal access tokens. A failed or unknown
+     * transport outcome can leave the deletion committed; look up the user
+     * before retrying.
+     *
+     * @param user User to delete, addressed by numeric ID or name.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the identifier is invalid; the user does not exist or is the
+     *         root user; the caller lacks manage-users permission; or the
+     *         request fails.
+     */
     void DeleteUser(const Identifier &user);
+
+    /**
+     * @brief Changes a user's name, status, and mutable settings.
+     *
+     * A supplied name must contain between 3 and 50 bytes and remain unique.
+     * Setting the status to inactive prevents subsequent authentication.
+     * Passing `std::nullopt` for the name or status leaves that field
+     * unchanged. The supplied UserUpdateOptions changes only the settings it
+     * contains; settings left unset retain their current values. User settings
+     * are currently not mutable, so the options object must be empty. Passing
+     * `std::nullopt` for both fields and an empty options object is accepted as
+     * a no-op.
+     *
+     * A failed or unknown transport outcome can leave the update committed.
+     * Retrieve the user before retrying with different values.
+     *
+     * @param user User to update, addressed by numeric ID or name.
+     * @param username New unique name, or `std::nullopt` to retain the name.
+     * @param status New authentication status, or `std::nullopt` to retain the
+     *        status.
+     * @param options User update options.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         an identifier, name, status, or option is invalid; an option is
+     *         unsupported; the user does not exist; the name is already in
+     *         use; the caller lacks manage-users permission; or the request
+     *         fails.
+     */
     void UpdateUser(const Identifier &user,
-                    std::optional<std::string> username = std::nullopt,
-                    std::optional<UserStatus> status    = std::nullopt);
+                    std::optional<std::string> username,
+                    std::optional<UserStatus> status,
+                    const UserUpdateOptions &options);
 
     /**
      * @brief Creates a top-level stream in the cluster metadata.
@@ -2950,6 +3761,28 @@
      *         request fails.
      */
     void DeletePartitions(const Identifier &stream, const Identifier &topic, std::uint32_t partitions_count);
+
+    /**
+     * @brief Deletes the oldest sealed segments from one partition.
+     *
+     * The active segment is never deleted. If fewer sealed segments exist than
+     * requested, every sealed segment is selected. A count of zero, or a
+     * partition with no sealed segments, succeeds without deleting data. The
+     * server commits a truncation watermark before local replicas remove the
+     * selected segment files.
+     *
+     * A failed or unknown transport outcome can leave the truncation committed.
+     * Inspect the partition before retrying this destructive request.
+     *
+     * @param stream Parent stream, addressed by numeric ID or name.
+     * @param topic Parent topic, addressed by numeric ID or name.
+     * @param partition_id Numeric partition ID.
+     * @param segments_count Maximum number of oldest sealed segments to delete.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         an identifier or partition is invalid; the stream, topic, or
+     *         partition does not exist; the caller lacks topic-management
+     *         permission; or the request fails.
+     */
     void DeleteSegments(const Identifier &stream,
                         const Identifier &topic,
                         std::uint32_t partition_id,
@@ -3153,11 +3986,80 @@
                               const Identifier &topic,
                               std::optional<std::uint32_t> partition_id = std::nullopt);
 
+    /**
+     * @brief Retrieves details for this client connection.
+     *
+     * The result includes consumer-group memberships observed for the current
+     * authenticated connection. HTTP is stateless and does not expose a
+     * persistent current connection, so the HTTP transport reports this
+     * operation as unavailable.
+     *
+     * @return Details for the current client connection.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the transport does not support the operation; or the request
+     *         fails.
+     */
     ClientInfoDetails GetMe();
+
+    /**
+     * @brief Retrieves one currently connected client by numeric ID.
+     *
+     * The result is a snapshot containing the connection's observed consumer-
+     * group memberships. The connection can disappear immediately after the
+     * request completes.
+     *
+     * @param client_id Server-assigned connection ID.
+     * @return Details for the requested client connection.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the requested connection does not exist; the caller lacks
+     *         read-servers or manage-servers permission; or the request fails.
+     */
     ClientInfoDetails GetClient(std::uint32_t client_id);
+
+    /**
+     * @brief Lists client connections currently known to the server.
+     *
+     * Each entry is a snapshot summary and omits individual consumer-group
+     * identifiers. Use GetClient() for membership details.
+     *
+     * @return Client connection summaries observed for this request.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the caller lacks read-servers or manage-servers permission; or
+     *         the request fails.
+     */
     std::vector<ClientInfo> GetClients();
 
+    /**
+     * @brief Requests an immediate flush of a partition's unsaved buffer.
+     *
+     * When supported, the server writes buffered messages to storage and also
+     * requests an operating-system synchronization when @p fsync is true. The
+     * current VSR server does not expose this maintenance primitive and returns
+     * a feature-unavailable error; its HTTP API has no matching route.
+     *
+     * @param stream Parent stream, addressed by numeric ID or name.
+     * @param topic Parent topic, addressed by numeric ID or name.
+     * @param partition_id Numeric partition ID.
+     * @param fsync Whether to request synchronization to stable storage.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         an identifier or partition is invalid; the server or transport
+     *         does not support the operation; the caller lacks permission; or
+     *         the request fails.
+     */
     void FlushUnsavedBuffer(const Identifier &stream, const Identifier &topic, std::uint32_t partition_id, bool fsync);
+
+    /**
+     * @brief Retrieves server process, storage, and resource statistics.
+     *
+     * The returned fields are an observed snapshot and can change immediately.
+     * Process and host measurements describe the server that handles the
+     * request; metadata totals describe the state visible to that server.
+     *
+     * @return Statistics observed for this request.
+     * @throws IggyException if the client is unavailable or unauthenticated;
+     *         the caller lacks read-servers or manage-servers permission; or
+     *         the request fails.
+     */
     Stats GetStats();
 
   private:
diff --git a/foreign/cpp/src/client.cpp b/foreign/cpp/src/client.cpp
index 0d101f8..bf01dce 100644
--- a/foreign/cpp/src/client.cpp
+++ b/foreign/cpp/src/client.cpp
@@ -137,10 +137,13 @@
 
 void IggyBlockingClient::UpdateUser(const Identifier &user,
                                     const std::optional<std::string> username,
-                                    const std::optional<UserStatus> status) {
-    RethrowAsIggyException([this, &user, &username, status] {
+                                    const std::optional<UserStatus> status,
+                                    const UserUpdateOptions &options) {
+    RethrowAsIggyException([this, &user, &username, status, &options] {
+        auto ffi_options = ToFfiRawOptions(options.RawEntries());
         Handle()->update_user(user.ToFfi(), username.has_value(), username.value_or(""), status.has_value(),
-                              static_cast<ffi::UserStatus>(status.value_or(UserStatus::Active)));
+                              static_cast<ffi::UserStatus>(status.value_or(UserStatus::Active)),
+                              std::move(ffi_options));
     });
 }
 
diff --git a/foreign/cpp/src/client.rs b/foreign/cpp/src/client.rs
index 6ea8fdc..c74fcfd 100644
--- a/foreign/cpp/src/client.rs
+++ b/foreign/cpp/src/client.rs
@@ -1164,6 +1164,7 @@
         username: String,
         has_status: bool,
         status: ffi::UserStatus,
+        options: Vec<ffi::HeaderEntry>,
     ) -> Result<(), String> {
         let rust_user_id = RustIdentifier::try_from(user_id)
             .map_err(|error| format!("Could not update user: invalid user identifier: {error}"))?;
@@ -1171,6 +1172,9 @@
             .then(|| RustUserStatus::try_from(status))
             .transpose()
             .map_err(|error| format!("Could not update user '{rust_user_id}': {error}"))?;
+        let raw = ffi_options_to_raw(options)
+            .map_err(|error| format!("Could not update user '{rust_user_id}': {error}"))?;
+        let rust_options = UserUpdateOptions { raw };
 
         RUNTIME.block_on(async {
             self.inner
@@ -1178,7 +1182,7 @@
                     &rust_user_id,
                     has_username.then_some(username.as_str()),
                     rust_status,
-                    &UserUpdateOptions::default(),
+                    &rust_options,
                 )
                 .await
                 .map_err(|error| format!("Could not update user '{rust_user_id}': {error}"))?;
diff --git a/foreign/cpp/src/lib.rs b/foreign/cpp/src/lib.rs
index 3ed6d11..8dda770 100644
--- a/foreign/cpp/src/lib.rs
+++ b/foreign/cpp/src/lib.rs
@@ -382,6 +382,7 @@
         created_at: u64,
         status: UserStatus,
         username: String,
+        options: Vec<HeaderEntry>,
     }
 
     struct UserInfoDetails {
@@ -391,6 +392,7 @@
         username: String,
         has_permissions: bool,
         permissions: Permissions,
+        options: Vec<HeaderEntry>,
     }
 
     struct LoginInfo {
@@ -656,6 +658,7 @@
             username: String,
             has_status: bool,
             status: UserStatus,
+            options: Vec<HeaderEntry>,
         ) -> Result<()>;
         fn update_permissions(
             self: &Client,
diff --git a/foreign/cpp/src/type_conversion.rs b/foreign/cpp/src/type_conversion.rs
index 0d95a69..9c55c78 100644
--- a/foreign/cpp/src/type_conversion.rs
+++ b/foreign/cpp/src/type_conversion.rs
@@ -158,6 +158,7 @@
             created_at: user.created_at.as_micros(),
             status: ffi::UserStatus::from(user.status),
             username: user.username,
+            options: resource_options_to_ffi(&user.options, true),
         }
     }
 }
@@ -172,6 +173,7 @@
             username: user.username,
             has_permissions,
             permissions: ffi::Permissions::from(user.permissions.unwrap_or_default()),
+            options: resource_options_to_ffi(&user.options, true),
         }
     }
 }
diff --git a/foreign/cpp/src/type_conversions.cpp b/foreign/cpp/src/type_conversions.cpp
index fc7594c..7306e26 100644
--- a/foreign/cpp/src/type_conversions.cpp
+++ b/foreign/cpp/src/type_conversions.cpp
@@ -136,7 +136,8 @@
 
 UserInfo UserInfo::FromFfi(ffi::UserInfo user) {
     return UserInfo(user.id, user.created_at, static_cast<UserStatus>(user.status),
-                    std::string(user.username.c_str(), user.username.size()));
+                    std::string(user.username.c_str(), user.username.size()),
+                    ResourceOptions::FromFfi(std::move(user.options), rust::Vec<ffi::HeaderEntry>{}));
 }
 
 UserInfoDetails UserInfoDetails::FromFfi(ffi::UserInfoDetails user) {
@@ -145,7 +146,8 @@
         permissions = ::iggy::Permissions::FromFfi(std::move(user.permissions));
     }
     return UserInfoDetails(user.id, user.created_at, static_cast<UserStatus>(user.status),
-                           std::string(user.username.c_str(), user.username.size()), std::move(permissions));
+                           std::string(user.username.c_str(), user.username.size()), std::move(permissions),
+                           ResourceOptions::FromFfi(std::move(user.options), rust::Vec<ffi::HeaderEntry>{}));
 }
 
 ffi::Identifier Identifier::ToFfi() const {
@@ -338,7 +340,7 @@
     std::vector<CacheMetricEntry> cache_metrics;
     cache_metrics.reserve(stats.cache_metrics.size());
     for (auto &entry : stats.cache_metrics) {
-        cache_metrics.push_back(CacheMetricEntry::FromFfi(std::move(entry)));
+        cache_metrics.push_back(CacheMetricEntry::FromFfi(entry));
     }
     return Stats(stats.process_id, stats.cpu_usage, stats.total_cpu_usage, stats.memory_usage, stats.total_memory,
                  stats.available_memory, stats.run_time_micros, stats.start_time_epoch_micros, stats.read_bytes,
diff --git a/foreign/cpp/tests/e2e/client.cpp b/foreign/cpp/tests/e2e/client.cpp
index 720770b..808e972 100644
--- a/foreign/cpp/tests/e2e/client.cpp
+++ b/foreign/cpp/tests/e2e/client.cpp
@@ -632,18 +632,22 @@
         const auto created = CreateUser(root_client, username, "secret123", iggy::UserStatus::Active);
 
         auto client = GetLoggedOutHighLevelClient();
-        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive),
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
+                                       iggy::UserUpdateOptions{}),
                      std::exception);
         ASSERT_NO_THROW(client.Connect());
-        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive),
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
+                                       iggy::UserUpdateOptions{}),
                      std::exception);
         ASSERT_NO_THROW(client.Login("iggy", "iggy"));
         ASSERT_NO_THROW(client.Logout());
-        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive),
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
+                                       iggy::UserUpdateOptions{}),
                      std::exception);
         ASSERT_NO_THROW(client.Login("iggy", "iggy"));
         ASSERT_NO_THROW(client.Disconnect());
-        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive),
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
+                                       iggy::UserUpdateOptions{}),
                      std::exception);
 
         const auto fetched = root_client.GetUser(iggy::Identifier::String(username));
@@ -660,12 +664,12 @@
     const std::string proposed_username = GetRandomName(50);
     const auto unknown_id               = std::numeric_limits<std::uint32_t>::max();
 
-    ASSERT_THROW(
-        client.UpdateUser(iggy::Identifier::String(unknown_username), proposed_username, iggy::UserStatus::Inactive),
-        std::exception);
-    ASSERT_THROW(
-        client.UpdateUser(iggy::Identifier::Numeric(unknown_id), GetRandomName(50), iggy::UserStatus::Inactive),
-        std::exception);
+    ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(unknown_username), proposed_username,
+                                   iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}),
+                 std::exception);
+    ASSERT_THROW(client.UpdateUser(iggy::Identifier::Numeric(unknown_id), GetRandomName(50), iggy::UserStatus::Inactive,
+                                   iggy::UserUpdateOptions{}),
+                 std::exception);
     ASSERT_THROW(client.GetUser(iggy::Identifier::String(proposed_username)), std::exception);
 }
 
@@ -676,7 +680,8 @@
     const std::string replacement = GetRandomName(50);
     ASSERT_NO_THROW({
         const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
-        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
+                                          iggy::UserUpdateOptions{}));
         RenameTrackedUser(username, replacement);
 
         ASSERT_THROW(client.GetUser(iggy::Identifier::String(username)), std::exception);
@@ -694,8 +699,8 @@
     const std::string replacement = GetRandomName(50);
     ASSERT_NO_THROW({
         const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Inactive);
-        ASSERT_NO_THROW(
-            client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement, iggy::UserStatus::Active));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement,
+                                          iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
         RenameTrackedUser(username, replacement);
 
         const auto by_id   = client.GetUser(iggy::Identifier::Numeric(created.Id()));
@@ -717,13 +722,14 @@
     ASSERT_NO_THROW({
         const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
 
-        ASSERT_NO_THROW(
-            client.UpdateUser(iggy::Identifier::Numeric(created.Id()), std::nullopt, iggy::UserStatus::Inactive));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), std::nullopt,
+                                          iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}));
         const auto status_updated = client.GetUser(iggy::Identifier::Numeric(created.Id()));
         EXPECT_EQ(status_updated.Username(), username);
         EXPECT_EQ(status_updated.Status(), iggy::UserStatus::Inactive);
 
-        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement, std::nullopt));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement, std::nullopt,
+                                          iggy::UserUpdateOptions{}));
         RenameTrackedUser(username, replacement);
 
         const auto username_updated = client.GetUser(iggy::Identifier::Numeric(created.Id()));
@@ -744,11 +750,11 @@
     ASSERT_NO_THROW({
         const auto first  = CreateUser(client, first_username, "secret123", iggy::UserStatus::Active);
         const auto second = CreateUser(client, second_username, "secret123", iggy::UserStatus::Active);
-        ASSERT_NO_THROW(
-            client.UpdateUser(iggy::Identifier::String(first_username), first_replacement, iggy::UserStatus::Active));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(first_username), first_replacement,
+                                          iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
         RenameTrackedUser(first_username, first_replacement);
-        ASSERT_NO_THROW(
-            client.UpdateUser(iggy::Identifier::String(second_username), second_replacement, iggy::UserStatus::Active));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(second_username), second_replacement,
+                                          iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
         RenameTrackedUser(second_username, second_replacement);
 
         const auto fetched_first  = client.GetUser(iggy::Identifier::String(first_replacement));
@@ -770,7 +776,8 @@
         const auto created = CreateUser(client, source, "secret123", iggy::UserStatus::Active);
 
         for (const auto &replacement : invalid_usernames) {
-            ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(source), replacement, iggy::UserStatus::Active),
+            ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(source), replacement, iggy::UserStatus::Active,
+                                           iggy::UserUpdateOptions{}),
                          std::exception);
         }
         const auto fetched = client.GetUser(iggy::Identifier::String(source));
@@ -793,7 +800,7 @@
         ASSERT_NO_THROW({
             const auto created = CreateUser(client, sources[index], "secret123", iggy::UserStatus::Active);
             ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(sources[index]), replacements[index],
-                                              iggy::UserStatus::Active));
+                                              iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
             RenameTrackedUser(sources[index], replacements[index]);
             const auto fetched = client.GetUser(iggy::Identifier::String(replacements[index]));
             EXPECT_EQ(fetched.Id(), created.Id());
@@ -815,7 +822,9 @@
         const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
         for (const auto status : statuses) {
             const std::string replacement = GetRandomName(50);
-            ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, status), std::exception);
+            ASSERT_THROW(
+                client.UpdateUser(iggy::Identifier::String(username), replacement, status, iggy::UserUpdateOptions{}),
+                std::exception);
             ASSERT_THROW(client.GetUser(iggy::Identifier::String(replacement)), std::exception);
         }
         const auto fetched = client.GetUser(iggy::Identifier::String(username));
@@ -833,9 +842,9 @@
     ASSERT_NO_THROW({
         const auto target   = CreateUser(client, target_username, "secret123", iggy::UserStatus::Active);
         const auto conflict = CreateUser(client, conflict_username, "secret123", iggy::UserStatus::Inactive);
-        ASSERT_THROW(
-            client.UpdateUser(iggy::Identifier::String(target_username), conflict_username, iggy::UserStatus::Inactive),
-            std::exception);
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(target_username), conflict_username,
+                                       iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}),
+                     std::exception);
         const auto fetched_target   = client.GetUser(iggy::Identifier::String(target_username));
         const auto fetched_conflict = client.GetUser(iggy::Identifier::String(conflict_username));
         EXPECT_EQ(fetched_target.Id(), target.Id());
@@ -851,7 +860,8 @@
     const std::string username = GetRandomName(50);
     ASSERT_NO_THROW({
         const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
-        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive,
+                                          iggy::UserUpdateOptions{}));
         const auto fetched = client.GetUser(iggy::Identifier::String(username));
         EXPECT_EQ(fetched.Id(), created.Id());
         EXPECT_EQ(fetched.Status(), iggy::UserStatus::Inactive);
@@ -872,8 +882,8 @@
     permissions.SetGlobal(global);
     ASSERT_NO_THROW({
         const auto created = CreateUser(root_client, username, "known-secret", iggy::UserStatus::Active, permissions);
-        ASSERT_NO_THROW(
-            root_client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Active));
+        ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), replacement,
+                                               iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
         RenameTrackedUser(username, replacement);
         const auto fetched = root_client.GetUser(iggy::Identifier::String(replacement));
         EXPECT_EQ(fetched.Id(), created.Id());
@@ -895,10 +905,12 @@
     auto user_client           = GetLoggedOutHighLevelClient();
     const std::string username = GetRandomName(50);
     ASSERT_NO_THROW({ CreateUser(root_client, username, "known-secret", iggy::UserStatus::Active); });
-    ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive));
+    ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive,
+                                           iggy::UserUpdateOptions{}));
     ASSERT_NO_THROW(user_client.Connect());
     ASSERT_THROW(user_client.Login(username, "known-secret"), std::exception);
-    ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Active));
+    ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Active,
+                                           iggy::UserUpdateOptions{}));
     ASSERT_NO_THROW(user_client.Login(username, "known-secret"));
 }
 
@@ -909,8 +921,8 @@
     const std::string new_username = GetRandomName(50);
     ASSERT_NO_THROW({
         const auto first = CreateUser(client, old_username, "secret123", iggy::UserStatus::Active);
-        ASSERT_NO_THROW(
-            client.UpdateUser(iggy::Identifier::String(old_username), new_username, iggy::UserStatus::Active));
+        ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(old_username), new_username,
+                                          iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
         RenameTrackedUser(old_username, new_username);
         const auto second = CreateUser(client, old_username, "secret123", iggy::UserStatus::Active);
         EXPECT_EQ(client.GetUser(iggy::Identifier::String(new_username)).Username(), new_username);
@@ -925,7 +937,8 @@
     const std::string username = GetRandomName(50);
     ASSERT_NO_THROW({
         const auto target = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
-        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), "iggy", iggy::UserStatus::Inactive),
+        ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), "iggy", iggy::UserStatus::Inactive,
+                                       iggy::UserUpdateOptions{}),
                      std::exception);
         const auto root    = client.GetUser(iggy::Identifier::String("iggy"));
         const auto fetched = client.GetUser(iggy::Identifier::String(username));
@@ -955,10 +968,10 @@
     ASSERT_NO_THROW(caller_client->connect());
     ASSERT_NO_THROW(caller_client->login_user(caller_username, "secret123"));
     ASSERT_THROW(caller_client->update_user(make_string_identifier(target_username), true, GetRandomName(50), true,
-                                            iggy::ffi::UserStatus::Inactive),
+                                            iggy::ffi::UserStatus::Inactive, {}),
                  std::exception);
     ASSERT_THROW(caller_client->update_user(make_string_identifier(caller_username), true, GetRandomName(50), true,
-                                            iggy::ffi::UserStatus::Inactive),
+                                            iggy::ffi::UserStatus::Inactive, {}),
                  std::exception);
     iggy::ffi::UserInfoDetails fetched{};
     ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(target_username)); });
@@ -984,7 +997,7 @@
     ASSERT_NO_THROW(manager_client->connect());
     ASSERT_NO_THROW(manager_client->login_user(manager_username, "secret123"));
     ASSERT_NO_THROW(manager_client->update_user(make_string_identifier(target_username), true, replacement, true,
-                                                iggy::ffi::UserStatus::Inactive));
+                                                iggy::ffi::UserStatus::Inactive, {}));
     RenameTrackedUser(target_username, replacement);
     iggy::ffi::UserInfoDetails fetched{};
     ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(replacement)); });
diff --git a/foreign/cpp/tests/unit/unit_tests.cpp b/foreign/cpp/tests/unit/unit_tests.cpp
index 916ffdb..16af7af 100644
--- a/foreign/cpp/tests/unit/unit_tests.cpp
+++ b/foreign/cpp/tests/unit/unit_tests.cpp
@@ -365,7 +365,8 @@
     EXPECT_THROW((void)client.GetUsers(), iggy::IggyException);
     EXPECT_THROW((void)client.CreateUser("user", "secret123", iggy::UserStatus::Active), iggy::IggyException);
     EXPECT_THROW(client.DeleteUser(stream), iggy::IggyException);
-    EXPECT_THROW(client.UpdateUser(stream, "updated-user", iggy::UserStatus::Active), iggy::IggyException);
+    EXPECT_THROW(client.UpdateUser(stream, "updated-user", iggy::UserStatus::Active, iggy::UserUpdateOptions{}),
+                 iggy::IggyException);
     EXPECT_THROW(client.CreateStream("stream"), iggy::IggyException);
     EXPECT_THROW(client.UpdateStream(stream, "updated-stream"), iggy::IggyException);
     EXPECT_THROW(client.GetStreams(), iggy::IggyException);