diff --git a/Sources/XXClient/Callbacks/GroupChatProcessor.swift b/Sources/XXClient/Callbacks/GroupChatProcessor.swift
index f303aae49186ab75df7a796f735e129027367ec6..a43ec39786fcab1de0d0ceaec978446b57c821c3 100644
--- a/Sources/XXClient/Callbacks/GroupChatProcessor.swift
+++ b/Sources/XXClient/Callbacks/GroupChatProcessor.swift
@@ -2,6 +2,8 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct GroupChatProcessor {
+  public typealias Result = Swift.Result<Callback, NSError>
+
   public struct Callback: Equatable {
     public init(
       decryptedMessage: GroupChatMessage,
@@ -29,14 +31,14 @@ public struct GroupChatProcessor {
 
   public init(
     name: String = "GroupChatProcessor",
-    handle: @escaping (Result<Callback, NSError>) -> Void
+    handle: @escaping (Result) -> Void
   ) {
     self.name = name
     self.handle = handle
   }
 
   public var name: String
-  public var handle: (Result<Callback, NSError>) -> Void
+  public var handle: (Result) -> Void
 }
 
 extension GroupChatProcessor {
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index 41e3cb1625df0053aefc702df9f176416f35645b..4e83f83b270368c65628cf42d18fcd2c53d32a66 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -18,6 +18,7 @@ public struct MessengerEnvironment {
   public var getFileTransferParams: GetFileTransferParams
   public var getNotificationsReport: GetNotificationsReport
   public var getSingleUseParams: GetSingleUseParams
+  public var groupChatProcessors: GroupChatProcessorRegistry
   public var groupRequests: GroupRequestCallbacksRegistry
   public var initFileTransfer: InitFileTransfer
   public var initializeBackup: InitializeBackup
@@ -72,6 +73,7 @@ extension MessengerEnvironment {
       getFileTransferParams: .liveDefault,
       getNotificationsReport: .live,
       getSingleUseParams: .liveDefault,
+      groupChatProcessors: .live(),
       groupRequests: .live(),
       initFileTransfer: .live,
       initializeBackup: .live,
@@ -121,6 +123,7 @@ extension MessengerEnvironment {
     getFileTransferParams: .unimplemented,
     getNotificationsReport: .unimplemented,
     getSingleUseParams: .unimplemented,
+    groupChatProcessors: .unimplemented,
     groupRequests: .unimplemented,
     initFileTransfer: .unimplemented,
     initializeBackup: .unimplemented,
diff --git a/Sources/XXMessengerClient/Utils/GroupChatProcessorRegistry.swift b/Sources/XXMessengerClient/Utils/GroupChatProcessorRegistry.swift
new file mode 100644
index 0000000000000000000000000000000000000000..48390cea619ea81dc76f990b5bce6d6db2de04ca
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/GroupChatProcessorRegistry.swift
@@ -0,0 +1,45 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct GroupChatProcessorRegistry {
+  public var register: (GroupChatProcessor) -> Cancellable
+  public var registered: () -> GroupChatProcessor
+}
+
+extension GroupChatProcessorRegistry {
+  public static func live() -> GroupChatProcessorRegistry {
+    class Registry {
+      var items: [UUID: GroupChatProcessor] = [:]
+    }
+    let registry = Registry()
+    return GroupChatProcessorRegistry(
+      register: { processor in
+        let id = UUID()
+        registry.items[id] = processor
+        return Cancellable { registry.items[id] = nil }
+      },
+      registered: {
+        GroupChatProcessor(
+          name: "GroupChatProcessorRegistry.registered",
+          handle: { result in
+            registry.items.values.forEach { $0.handle(result) }
+          }
+        )
+      }
+    )
+  }
+}
+
+extension GroupChatProcessorRegistry {
+  public static let unimplemented = GroupChatProcessorRegistry(
+    register: XCTestDynamicOverlay.unimplemented(
+      "\(Self.self).register",
+      placeholder: Cancellable {}
+    ),
+    registered: XCTestDynamicOverlay.unimplemented(
+      "\(Self.self).registered",
+      placeholder: .unimplemented
+    )
+  )
+}
diff --git a/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
index bf4bdf72154944bb36b52e7bda74ae76ecce7757..df7a6271d72996ba549bc4928628124a5dc5c811 100644
--- a/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
+++ b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
@@ -63,3 +63,28 @@ extension Group {
     return group
   }
 }
+
+extension GroupChatMessage {
+  static func stub() -> GroupChatMessage {
+    GroupChatMessage(
+      groupId: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      senderId: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      messageId: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      payload: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      timestamp: Int64.random(in: 100...999)
+    )
+  }
+}
+
+extension GroupChatProcessor.Callback {
+  static func stub() -> GroupChatProcessor.Callback {
+    GroupChatProcessor.Callback(
+      decryptedMessage: .stub(),
+      msg: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      receptionId: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      ephemeralId: Int64.random(in: 100...999),
+      roundId: Int64.random(in: 100...999),
+      roundUrl: "\(Int.random(in: 100...999))"
+    )
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Utils/GroupChatProcessorRegistryTests.swift b/Tests/XXMessengerClientTests/Utils/GroupChatProcessorRegistryTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..85ab46d913d117adad8bd0b9140f0f299c3a7a7d
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Utils/GroupChatProcessorRegistryTests.swift
@@ -0,0 +1,48 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class GroupChatProcessorRegistryTests: XCTestCase {
+  func testRegistry() {
+    var firstProcessorDidHandle: [GroupChatProcessor.Result] = []
+    var secondProcessorDidHandle: [GroupChatProcessor.Result] = []
+
+    let firstProcessor = GroupChatProcessor(
+      name: "first",
+      handle: { firstProcessorDidHandle.append($0) }
+    )
+    let secondProcessor = GroupChatProcessor(
+      name: "second",
+      handle: { secondProcessorDidHandle.append($0) }
+    )
+    let registry: GroupChatProcessorRegistry = .live()
+    let registeredProcessors = registry.registered()
+    let firstProcessorCancellable = registry.register(firstProcessor)
+    let secondProcessorCancellable = registry.register(secondProcessor)
+
+    let firstResult = GroupChatProcessor.Result.success(.stub())
+    registeredProcessors.handle(firstResult)
+
+    XCTAssertNoDifference(firstProcessorDidHandle, [firstResult])
+    XCTAssertNoDifference(secondProcessorDidHandle, [firstResult])
+
+    firstProcessorDidHandle = []
+    secondProcessorDidHandle = []
+    firstProcessorCancellable.cancel()
+    let secondResult = GroupChatProcessor.Result.success(.stub())
+    registeredProcessors.handle(secondResult)
+
+    XCTAssertNoDifference(firstProcessorDidHandle, [])
+    XCTAssertNoDifference(secondProcessorDidHandle, [secondResult])
+
+    firstProcessorDidHandle = []
+    secondProcessorDidHandle = []
+    secondProcessorCancellable.cancel()
+    let thirdResult = GroupChatProcessor.Result.success(.stub())
+    registeredProcessors.handle(thirdResult)
+
+    XCTAssertNoDifference(firstProcessorDidHandle, [])
+    XCTAssertNoDifference(secondProcessorDidHandle, [])
+  }
+}