diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj
index 11e679ee9..7c5ee60b3 100644
--- a/Session.xcodeproj/project.pbxproj
+++ b/Session.xcodeproj/project.pbxproj
@@ -177,6 +177,7 @@
 		7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; };
 		7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; };
+		7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD477A727EC39F5004E2822 /* Atomic.swift */; };
 		7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; };
 		7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
 		7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; };
@@ -1198,6 +1199,7 @@
 		7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = "<group>"; };
 		7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = "<group>"; };
+		7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
 		7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = "<group>"; };
 		7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; };
 		7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; };
@@ -2433,6 +2435,7 @@
 		B8A582B0258C66C900AFD84C /* General */ = {
 			isa = PBXGroup;
 			children = (
+				7BD477A727EC39F5004E2822 /* Atomic.swift */,
 				7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */,
 				7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */,
 				7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */,
@@ -4796,6 +4799,7 @@
 				C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
 				B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
 				C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
+				7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
 				B8BC00C0257D90E30032E807 /* General.swift in Sources */,
 				C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
 				C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */,
diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift
index 9fbb5c73e..a5fcce51c 100644
--- a/Session/Conversations/ConversationVC+Interaction.swift
+++ b/Session/Conversations/ConversationVC+Interaction.swift
@@ -283,49 +283,46 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
         let linkPreviewDraft = snInputView.linkPreviewInfo?.draft
         let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
         
-        Storage.write(with: { transaction in
-            let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
-                for: self.thread,
-                with: transaction,
-                isNewThread: !oldThreadShouldBeVisible,
-                timestamp: (sentTimestamp - 1)  // Set 1ms earlier as this is used for sorting
-            )
-            .map { [weak self] _ in
-                self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
-        
-                Storage.write(with: { transaction in
-                    message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
-                }, completion: { [weak self] in
-                    tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
-                    
-                    Storage.shared.write(
-                        with: { transaction in
-                            tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
-                        },
-                        completion: { [weak self] in
-                            // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
-                            // the height of the new message cell
-                            self?.scrollToBottom(isAnimated: false)
-                        }
-                    )
-                    
-                    Storage.shared.write { transaction in
-                        MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
+        let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
+            for: self.thread,
+            isNewThread: !oldThreadShouldBeVisible,
+            timestamp: (sentTimestamp - 1)  // Set 1ms earlier as this is used for sorting
+        )
+        .map { [weak self] _ in
+            self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
+    
+            Storage.write(with: { transaction in
+                message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
+            }, completion: { [weak self] in
+                tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
+                
+                Storage.shared.write(
+                    with: { transaction in
+                        tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
+                    },
+                    completion: { [weak self] in
+                        // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
+                        // the height of the new message cell
+                        self?.scrollToBottom(isAnimated: false)
                     }
-                    
-                    self?.handleMessageSent()
-                })
-            }
-            
-            // Show an error indicating that approving the thread failed
-            promise.catch(on: DispatchQueue.main) { [weak self] _ in
-                let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
-                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
-                self?.present(alert, animated: true, completion: nil)
-            }
-            
-            promise.retainUntilComplete()
-        })
+                )
+                
+                Storage.shared.write { transaction in
+                    MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
+                }
+                
+                self?.handleMessageSent()
+            })
+        }
+        
+        // Show an error indicating that approving the thread failed
+        promise.catch(on: DispatchQueue.main) { [weak self] _ in
+            let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+            self?.present(alert, animated: true, completion: nil)
+        }
+        
+        promise.retainUntilComplete()
     }
 
     func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) {
@@ -349,44 +346,41 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
         let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible
         let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
         
-        Storage.write(with: { transaction in
-            let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
-                for: self.thread,
-                with: transaction,
-                isNewThread: !oldThreadShouldBeVisible,
-                timestamp: (sentTimestamp - 1)  // Set 1ms earlier as this is used for sorting
+        let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
+            for: self.thread,
+            isNewThread: !oldThreadShouldBeVisible,
+            timestamp: (sentTimestamp - 1)  // Set 1ms earlier as this is used for sorting
+        )
+        .map { [weak self] _ in
+            Storage.write(
+                with: { transaction in
+                    tsMessage.save(with: transaction)
+                    // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
+                },
+                completion: { [weak self] in
+                    Storage.write(with: { transaction in
+                        MessageSender.send(message, with: attachments, in: thread, using: transaction)
+                    }, completion: { [weak self] in
+                        // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
+                        // the height of the new message cell
+                        self?.scrollToBottom(isAnimated: false)
+                    })
+                    self?.handleMessageSent()
+            
+                    // Attachment successfully sent - dismiss the screen
+                    onComplete?()
+                }
             )
-            .map { [weak self] _ in
-                Storage.write(
-                    with: { transaction in
-                        tsMessage.save(with: transaction)
-                        // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
-                    },
-                    completion: { [weak self] in
-                        Storage.write(with: { transaction in
-                            MessageSender.send(message, with: attachments, in: thread, using: transaction)
-                        }, completion: { [weak self] in
-                            // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
-                            // the height of the new message cell
-                            self?.scrollToBottom(isAnimated: false)
-                        })
-                        self?.handleMessageSent()
-                
-                        // Attachment successfully sent - dismiss the screen
-                        onComplete?()
-                    }
-                )
-            }
+        }
+    
+        // Show an error indicating that approving the thread failed
+        promise.catch(on: DispatchQueue.main) { [weak self] _ in
+            let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+            self?.present(alert, animated: true, completion: nil)
+        }
         
-            // Show an error indicating that approving the thread failed
-            promise.catch(on: DispatchQueue.main) { [weak self] _ in
-                let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
-                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
-                self?.present(alert, animated: true, completion: nil)
-            }
-            
-            promise.retainUntilComplete()
-        })
+        promise.retainUntilComplete()
     }
 
     func handleMessageSent() {
@@ -1128,7 +1122,7 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate {
 
 extension ConversationVC {
     
-    fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
+    fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
         guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
         
         // If the contact doesn't exist then we should create it so we can store the 'isApproved' state
@@ -1159,7 +1153,17 @@ extension ConversationVC {
                 }
                 
                 return promise
-                    .then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) }
+                    .then { _ -> Promise<Void> in
+                        let (promise, seal) = Promise<Void>.pending()
+                        Storage.writeSync { transaction in
+                            MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction)
+                                .done { seal.fulfill(()) }
+                                .catch { _ in seal.fulfill(()) } // Fulfill even if this failed; the configuration in the swarm should be at most 2 days old
+                                .retainUntilComplete()
+                        }
+                        
+                        return promise
+                    }
                     .map { _ in
                         if self?.presentedViewController is ModalActivityIndicatorViewController {
                             self?.dismiss(animated: true, completion: nil) // Dismiss the loader
@@ -1168,9 +1172,11 @@ extension ConversationVC {
             }
             .map { _ in
                 // Default 'didApproveMe' to true for the person approving the message request
-                contact.isApproved = true
-                contact.didApproveMe = (contact.didApproveMe || !isNewThread)
-                Storage.shared.setContact(contact, using: transaction)
+                Storage.write { transaction in
+                    contact.isApproved = true
+                    contact.didApproveMe = (contact.didApproveMe || !isNewThread)
+                    Storage.shared.setContact(contact, using: transaction)
+                }
                 
                 // Hide the 'messageRequestView' since the request has been approved and force a config
                 // sync to propagate the contact approval state (both must run on the main thread)
@@ -1208,30 +1214,27 @@ extension ConversationVC {
                 
                     // Send a sync message with the details of the contact
                     if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
-                        appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete()
+                        appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
                     }
                 }
             }
     }
     
     @objc func acceptMessageRequest() {
-        Storage.write { transaction in
-            let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
-                for: self.thread,
-                with: transaction,
-                isNewThread: false,
-                timestamp: NSDate.millisecondTimestamp()
-            )
-            
-            // Show an error indicating that approving the thread failed
-            promise.catch(on: DispatchQueue.main) { [weak self] _ in
-                let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
-                alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
-                self?.present(alert, animated: true, completion: nil)
-            }
-            
-            promise.retainUntilComplete()
+        let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
+            for: self.thread,
+            isNewThread: false,
+            timestamp: NSDate.millisecondTimestamp()
+        )
+        
+        // Show an error indicating that approving the thread failed
+        promise.catch(on: DispatchQueue.main) { [weak self] _ in
+            let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
+            self?.present(alert, animated: true, completion: nil)
         }
+        
+        promise.retainUntilComplete()
     }
     
     @objc func deleteMessageRequest() {
diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift
index 7eea99ca0..09a4fd257 100644
--- a/Session/Meta/AppDelegate.swift
+++ b/Session/Meta/AppDelegate.swift
@@ -144,10 +144,11 @@ extension AppDelegate {
         guard Storage.shared.getUser()?.name != nil else { return }
         let userDefaults = UserDefaults.standard
         let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast
-        guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60,
-            let configurationMessage = ConfigurationMessage.getCurrent() else { return } // Sync every 2 days
+        guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days
         let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
-        Storage.shared.write { transaction in
+        Storage.write { transaction in
+            guard let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { return }
+            
             let job = MessageSendJob(message: configurationMessage, destination: destination)
             JobQueue.shared.add(job, using: transaction)
         }
@@ -159,14 +160,17 @@ extension AppDelegate {
         }
     }
 
-    func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Void> {
-        guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
-            return Promise.value(())
-        }
-        
+    func forceSyncConfigurationNowIfNeeded() -> Promise<Void> {
         let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
         let (promise, seal) = Promise<Void>.pending()
+        
+        // Note: SQLite only supports a single write thread so we can be sure this will retrieve the most up-to-date data
         Storage.writeSync { transaction in
+            guard Storage.shared.getUser(using: transaction)?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
+                seal.fulfill(())
+                return
+            }
+            
             MessageSender.send(configurationMessage, to: destination, using: transaction).done {
                 seal.fulfill(())
             }.catch { _ in
diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift
index a5a3aef29..7577b37fb 100644
--- a/Session/Settings/NukeDataModal.swift
+++ b/Session/Settings/NukeDataModal.swift
@@ -135,7 +135,7 @@ final class NukeDataModal : Modal {
             appDelegate.forceSyncConfigurationNowIfNeeded().ensure(on: DispatchQueue.main) {
                 self?.dismiss(animated: true, completion: nil) // Dismiss the loader
                 UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
-                General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access
+                General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
                 NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
             }.retainUntilComplete()
         }
@@ -147,7 +147,7 @@ final class NukeDataModal : Modal {
                 self?.dismiss(animated: true, completion: nil) // Dismiss the loader
                 let potentiallyMaliciousSnodes = confirmations.compactMap { $0.value == false ? $0.key : nil }
                 if potentiallyMaliciousSnodes.isEmpty {
-                    General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access
+                    General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
                     UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
                     NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
                 } else {
diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift
index 201a34f6f..d8f5de95d 100644
--- a/SessionMessagingKit/Database/Storage+Shared.swift
+++ b/SessionMessagingKit/Database/Storage+Shared.swift
@@ -40,7 +40,7 @@ extension Storage {
     }
     
     public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? {
-        guard let userPublicKey = getUserPublicKey() else { return nil }
+        let userPublicKey = getUserHexEncodedPublicKey()
         var result: Contact?
         
         if let transaction = transaction {
diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift
index 641a055ae..da328bd28 100644
--- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift	
+++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift	
@@ -2,7 +2,7 @@ import SessionUtilitiesKit
 
 extension ConfigurationMessage {
 
-    public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? {
+    public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? {
         let storage = Storage.shared
         guard let user = storage.getUser(using: transaction) else { return nil }
         
@@ -13,94 +13,84 @@ extension ConfigurationMessage {
         var openGroups: Set<String> = []
         var contacts: Set<ConfigurationMessage.Contact> = []
         
-        let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in
-            TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
-                guard let thread = object as? TSGroupThread else { return }
-                
-                switch thread.groupModel.groupType {
-                    case .closedGroup:
-                        guard thread.isCurrentUserMemberInGroup() else { return }
-                        
-                        let groupID = thread.groupModel.groupId
-                        let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
-                        
-                        guard
-                            storage.isClosedGroup(groupPublicKey, using: transaction),
-                            let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
-                        else {
-                            return
-                        }
-                        
-                        let closedGroup = ClosedGroup(
-                            publicKey: groupPublicKey,
-                            name: (thread.groupModel.groupName ?? ""),
-                            encryptionKeyPair: encryptionKeyPair,
-                            members: Set(thread.groupModel.groupMemberIds),
-                            admins: Set(thread.groupModel.groupAdminIds),
-                            expirationTimer: thread.disappearingMessagesDuration(with: transaction)
-                        )
-                        closedGroups.insert(closedGroup)
-                        
-                    case .openGroup:
-                        if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) {
-                            openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
-                        }
-                        
-                    default: break
-                }
-            }
-            
-            let currentUserPublicKey: String = getUserHexEncodedPublicKey()
+        TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
+            guard let thread = object as? TSGroupThread else { return }
             
-            contacts = storage.getAllContacts(with: transaction)
-                .filter { contact -> Bool in
-                    let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
+            switch thread.groupModel.groupType {
+                case .closedGroup:
+                    guard thread.isCurrentUserMemberInGroup() else { return }
+                    
+                    let groupID = thread.groupModel.groupId
+                    let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
                     
-                    return (
-                        // Skip the current user
-                        contact.sessionID != currentUserPublicKey &&
-                        // Contacts which have visible threads
-                        TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
-                            
-                            // Include already approved contacts
-                            contact.isApproved ||
-                            contact.didApproveMe ||
-                            
-                            // Sync blocked contacts
-                            SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID)
-                        )
+                    guard
+                        storage.isClosedGroup(groupPublicKey, using: transaction),
+                        let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
+                    else {
+                        return
+                    }
+                    
+                    let closedGroup = ClosedGroup(
+                        publicKey: groupPublicKey,
+                        name: (thread.groupModel.groupName ?? ""),
+                        encryptionKeyPair: encryptionKeyPair,
+                        members: Set(thread.groupModel.groupMemberIds),
+                        admins: Set(thread.groupModel.groupAdminIds),
+                        expirationTimer: thread.disappearingMessagesDuration(with: transaction)
                     )
-                }
-                .map { contact -> ConfigurationMessage.Contact in
-                    // Can just default the 'hasX' values to true as they will be set to this
-                    // when converting to proto anyway
-                    let profilePictureURL = contact.profilePictureURL
-                    let profileKey = contact.profileEncryptionKey?.keyData
+                    closedGroups.insert(closedGroup)
+                    
+                case .openGroup:
+                    if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) {
+                        openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
+                    }
                     
-                    return ConfigurationMessage.Contact(
-                        publicKey: contact.sessionID,
-                        displayName: (contact.name ?? contact.sessionID),
-                        profilePictureURL: profilePictureURL,
-                        profileKey: profileKey,
-                        hasIsApproved: true,
-                        isApproved: contact.isApproved,
-                        hasIsBlocked: true,
-                        isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID),
-                        hasDidApproveMe: true,
-                        didApproveMe: contact.didApproveMe
+                default: break
+            }
+        }
+        
+        let currentUserPublicKey: String = getUserHexEncodedPublicKey()
+        
+        contacts = storage.getAllContacts(with: transaction)
+            .compactMap { contact -> ConfigurationMessage.Contact? in
+                let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
+                
+                guard
+                    // Skip the current user
+                    contact.sessionID != currentUserPublicKey &&
+                    // Contacts which have visible threads
+                    TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
+                        
+                        // Include already approved contacts
+                        contact.isApproved ||
+                        contact.didApproveMe ||
+                        
+                        // Sync blocked contacts
+                        SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID)
                     )
+                else {
+                    return nil
                 }
-                .asSet()
+                
+                // Can just default the 'hasX' values to true as they will be set to this
+                // when converting to proto anyway
+                let profilePictureURL = contact.profilePictureURL
+                let profileKey = contact.profileEncryptionKey?.keyData
+                
+                return ConfigurationMessage.Contact(
+                    publicKey: contact.sessionID,
+                    displayName: (contact.name ?? contact.sessionID),
+                    profilePictureURL: profilePictureURL,
+                    profileKey: profileKey,
+                    hasIsApproved: true,
+                    isApproved: contact.isApproved,
+                    hasIsBlocked: true,
+                    isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID),
+                    hasDidApproveMe: true,
+                    didApproveMe: contact.didApproveMe
+                )
             }
-        
-        // If we are provided with a transaction then read the data based on the state of the database
-        // from within the transaction rather than the state in disk
-        if let transaction: YapDatabaseReadWriteTransaction = transaction {
-            populateDataClosure(transaction)
-        }
-        else {
-            Storage.read { transaction in populateDataClosure(transaction) }
-        }
+            .asSet()
         
         return ConfigurationMessage(
             displayName: displayName,
diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift
index bd6cef3d2..6113c5b68 100644
--- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift	
+++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift	
@@ -869,12 +869,14 @@ extension MessageReceiver {
         // a new configuration message (otherwise the `contact` will be loaded direct from the database and the
         // `didApproveMe` value won't have been updated)
         DispatchQueue.global(qos: .background).async {
-            guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else {
-                return
+            Storage.write { transaction in
+                guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
+                    return
+                }
+                
+                let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
+                MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
             }
-            
-            let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
-            MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
         }
     }
     
diff --git a/SessionMessagingKit/Utilities/General.swift b/SessionMessagingKit/Utilities/General.swift
index 565338f65..d2e4e0c96 100644
--- a/SessionMessagingKit/Utilities/General.swift
+++ b/SessionMessagingKit/Utilities/General.swift
@@ -2,7 +2,7 @@ import Foundation
 
 public enum General {
     public enum Cache {
-        public static var cachedEncodedPublicKey: String? = nil
+        public static var cachedEncodedPublicKey: Atomic<String?> = Atomic(nil)
     }
 }
 
@@ -14,10 +14,10 @@ public class GeneralUtilities: NSObject {
 }
 
 public func getUserHexEncodedPublicKey() -> String {
-    if let cachedKey: String = General.Cache.cachedEncodedPublicKey { return cachedKey }
+    if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey }
     
     if let keyPair = OWSIdentityManager.shared().identityKeyPair() { // Can be nil under some circumstances
-        General.Cache.cachedEncodedPublicKey = keyPair.hexEncodedPublicKey
+        General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey }
         return keyPair.hexEncodedPublicKey
     }
     
diff --git a/SessionUtilitiesKit/General/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift
new file mode 100644
index 000000000..14baeed68
--- /dev/null
+++ b/SessionUtilitiesKit/General/Atomic.swift
@@ -0,0 +1,44 @@
+// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
+import Foundation
+
+// MARK: - Atomic<Value>
+/// The `Atomic<Value>` wrapper is a generic wrapper providing a thread-safe way to get and set a value
+///
+/// A write-up on the need for this class and it's approach can be found here:
+/// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/
+/// there is also another approach which can be taken but it requires separate types for collections and results in
+/// a somewhat inconsistent interface between different `Atomic` wrappers
+@propertyWrapper
+public class Atomic<Value> {
+    private let queue: DispatchQueue = DispatchQueue(label: "io.oxen.\(UUID().uuidString)")
+    private var value: Value
+
+    /// In order to change the value you **must** use the `mutate` function
+    public var wrappedValue: Value {
+        return queue.sync { return value }
+    }
+
+    /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections
+    public var projectedValue: Atomic<Value> {
+        return self
+    }
+
+    // MARK: - Initialization
+    public init(_ initialValue: Value) {
+        self.value = initialValue
+    }
+
+    // MARK: - Functions
+    
+    public func mutate(_ mutation: (inout Value) -> Void) {
+        return queue.sync {
+            mutation(&value)
+        }
+    }
+}
+
+extension Atomic where Value: CustomDebugStringConvertible {
+    var debugDescription: String {
+        return value.debugDescription
+    }
+}