diff --git a/LibSession-Util b/LibSession-Util
index 12df14a6f..e1a76ebb7 160000
--- a/LibSession-Util
+++ b/LibSession-Util
@@ -1 +1 @@
-Subproject commit 12df14a6fc4c3276c651dbc612377ff0d80fb323
+Subproject commit e1a76ebb7b8f90f4dc07e05ccaf88643da2829ad
diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift
index c2ccfac72..3bc1cac72 100644
--- a/Session/Calls/Call Management/SessionCallManager.swift	
+++ b/Session/Calls/Call Management/SessionCallManager.swift	
@@ -205,7 +205,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
             // Stop all jobs except for message sending and when completed suspend the database
             JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { _ in
                 LibSession.suspendNetworkAccess()
-                Storage.suspendDatabaseAccess(using: dependencies)
+                Storage.shared.suspendDatabaseAccess()
                 Log.flush()
             }
         }
diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift
index 155ec6bcd..1335012a0 100644
--- a/Session/Meta/AppDelegate.swift
+++ b/Session/Meta/AppDelegate.swift
@@ -147,7 +147,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         /// Apple's documentation on the matter)
         UNUserNotificationCenter.current().delegate = self
         
-        Storage.resumeDatabaseAccess(using: dependencies)
+        dependencies.storage.resumeDatabaseAccess()
         LibSession.resumeNetworkAccess()
         
         // Reset the 'startTime' (since it would be invalid from the last launch)
@@ -212,7 +212,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { [dependencies] neededBackgroundProcessing in
             if !self.hasCallOngoing() && (!neededBackgroundProcessing || Singleton.hasAppContext && Singleton.appContext.isInBackground) {
                 LibSession.suspendNetworkAccess()
-                Storage.suspendDatabaseAccess(using: dependencies)
+                dependencies.storage.suspendDatabaseAccess()
                 Log.info("[AppDelegate] completed network and database shutdowns.")
                 Log.flush()
             }
@@ -238,7 +238,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         UserDefaults.sharedLokiProject?[.isMainAppActive] = true
         
         // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background)
-        Storage.resumeDatabaseAccess(using: dependencies)
+        dependencies.storage.resumeDatabaseAccess()
         LibSession.resumeNetworkAccess()
         
         ensureRootViewController(calledFrom: .didBecomeActive)
@@ -288,7 +288,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
         Log.appResumedExecution()
         Log.info("Starting background fetch.")
-        Storage.resumeDatabaseAccess(using: dependencies)
+        dependencies.storage.resumeDatabaseAccess()
         LibSession.resumeNetworkAccess()
         
         let queue: DispatchQueue = .global(qos: .userInitiated)
@@ -312,7 +312,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             
             if Singleton.hasAppContext && Singleton.appContext.isInBackground {
                 LibSession.suspendNetworkAccess()
-                Storage.suspendDatabaseAccess(using: dependencies)
+                dependencies.storage.suspendDatabaseAccess()
                 Log.flush()
             }
             
@@ -338,7 +338,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                         
                         if Singleton.hasAppContext && Singleton.appContext.isInBackground {
                             LibSession.suspendNetworkAccess()
-                            Storage.suspendDatabaseAccess(using: dependencies)
+                            dependencies.storage.suspendDatabaseAccess()
                             Log.flush()
                         }
                         
@@ -471,7 +471,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             case .databaseError:
                 alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { [dependencies] _ in
                     // Reset the current database for a clean migration
-                    Storage.resetForCleanMigration()
+                    dependencies.storage.resetForCleanMigration()
                     
                     // Hide the top banner if there was one
                     TopBannerController.hide()
diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift
index 1dc53a1a3..fc02eb667 100644
--- a/Session/Meta/SessionApp.swift
+++ b/Session/Meta/SessionApp.swift
@@ -117,7 +117,7 @@ public struct SessionApp {
         LibSession.clearSnodeCache()
         LibSession.suspendNetworkAccess()
         PushNotificationAPI.resetKeys()
-        Storage.resetAllStorage()
+        dependencies.storage.resetAllStorage()
         ProfileManager.resetProfileStorage()
         Attachment.resetAttachmentStorage()
         AppEnvironment.shared.notificationPresenter.clearAllNotifications()
diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift
index 3c1f82531..309751259 100644
--- a/Session/Notifications/PushRegistrationManager.swift
+++ b/Session/Notifications/PushRegistrationManager.swift
@@ -293,7 +293,7 @@ public enum PushRegistrationError: Error {
         // FIXME: Initialise the `PushRegistrationManager` with a dependencies instance
         let dependencies: Dependencies = Dependencies()
         
-        Storage.resumeDatabaseAccess(using: dependencies)
+        Storage.shared.resumeDatabaseAccess()
         LibSession.resumeNetworkAccess()
         
         let maybeCall: SessionCall? = Storage.shared.write { db in
diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift
index f26373861..408720b60 100644
--- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift
+++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift
@@ -4,6 +4,7 @@
 
 import Foundation
 import GRDB
+import SessionSnodeKit
 import SessionUtil
 import SessionUtilitiesKit
 
@@ -256,4 +257,25 @@ public extension Crypto.Generator {
             return try decryptedData ?? { throw MessageReceiverError.decryptionFailed }()
         }
     }
+    
+    static func messageServerHash(
+        swarmPubkey: String,
+        namespace: SnodeAPI.Namespace,
+        data: Data
+    ) -> Crypto.Generator<String> {
+        return Crypto.Generator(
+            id: "messageServerHash",
+            args: [swarmPubkey, namespace, data]
+        ) {
+            var cSwarmPubkey: [CChar] = try swarmPubkey.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
+            var cData: [CChar] = try data.base64EncodedString().cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
+            var cHash: [CChar] = [CChar](repeating: 0, count: 65)
+            
+            guard session_compute_message_hash(cSwarmPubkey, Int16(namespace.rawValue), cData, &cHash) else {
+                throw MessageReceiverError.decryptionFailed
+            }
+            
+            return String(cString: cHash)
+        }
+    }
 }
diff --git a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift
index 57d73d0af..153a639ea 100644
--- a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift
+++ b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift
@@ -203,7 +203,7 @@ public extension ConfigurationSyncJob {
     static func enqueue(
         _ db: Database,
         publicKey: String,
-        dependencies: Dependencies = Dependencies()
+        using dependencies: Dependencies = Dependencies()
     ) {
         // Upsert a config sync job if needed
         dependencies.jobRunner.upsert(
diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift
index f342a8104..a8e1c962c 100644
--- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift	
+++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift	
@@ -21,6 +21,7 @@ public enum MessageReceiver {
         var customMessage: Message? = nil
         let sender: String
         let sentTimestamp: UInt64
+        let serverHash: String?
         let openGroupServerMessageId: UInt64?
         let threadVariant: SessionThread.Variant
         let threadIdGenerator: (Message) throws -> String
@@ -40,6 +41,7 @@ public enum MessageReceiver {
                 plaintext = data.removePadding()   // Remove the padding
                 sender = messageSender
                 sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency
+                serverHash = nil
                 openGroupServerMessageId = UInt64(messageServerId)
                 threadVariant = .community
                 threadIdGenerator = { message in
@@ -50,10 +52,6 @@ public enum MessageReceiver {
                 }
                 
             case (_, .openGroupInbox(let timestamp, let messageServerId, let serverPublicKey, let senderId, let recipientId)):
-                guard let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
-                    throw MessageReceiverError.noUserED25519KeyPair
-                }
-                
                 (plaintext, sender) = try dependencies.crypto.tryGenerate(
                     .plaintextWithSessionBlindingProtocol(
                         db,
@@ -67,11 +65,12 @@ public enum MessageReceiver {
                 
                 plaintext = plaintext.removePadding()   // Remove the padding
                 sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency
+                serverHash = nil
                 openGroupServerMessageId = UInt64(messageServerId)
                 threadVariant = .contact
                 threadIdGenerator = { _ in sender }
                 
-            case (_, .swarm(let publicKey, let namespace, _, _, _)):
+            case (_, .swarm(let publicKey, let namespace, let swarmServerHash, _, _)):
                 switch namespace {
                     case .default:
                         guard
@@ -81,9 +80,6 @@ public enum MessageReceiver {
                             SNLog("Failed to unwrap data for message from 'default' namespace.")
                             throw MessageReceiverError.invalidMessage
                         }
-                        guard let userX25519KeyPair: KeyPair = Identity.fetchUserKeyPair(db) else {
-                            throw MessageReceiverError.noUserX25519KeyPair
-                        }
                         
                         (plaintext, sender) = try dependencies.crypto.tryGenerate(
                             .plaintextWithSessionProtocol(
@@ -94,6 +90,7 @@ public enum MessageReceiver {
                         )
                         plaintext = plaintext.removePadding()   // Remove the padding
                         sentTimestamp = envelope.timestamp
+                        serverHash = swarmServerHash
                         openGroupServerMessageId = nil
                         threadVariant = .contact
                         threadIdGenerator = { message in
@@ -148,6 +145,16 @@ public enum MessageReceiver {
                         (plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs)
                         plaintext = plaintext.removePadding()   // Remove the padding
                         sentTimestamp = envelope.timestamp
+                        
+                        /// If we weren't given a `serverHash` then compute one locally using the same logic the swarm would
+                        switch swarmServerHash.isEmpty {
+                            case false: serverHash = swarmServerHash
+                            case true:
+                                serverHash = dependencies.crypto.generate(
+                                    .messageServerHash(swarmPubkey: publicKey, namespace: namespace, data: data)
+                                ).defaulting(to: "")
+                        }
+                        
                         openGroupServerMessageId = nil
                         threadVariant = .legacyGroup
                         threadIdGenerator = { _ in publicKey }
@@ -170,7 +177,7 @@ public enum MessageReceiver {
         let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender))
         message.sender = sender
         message.recipient = userSessionId
-        message.serverHash = origin.serverHash
+        message.serverHash = serverHash
         message.sentTimestamp = sentTimestamp
         message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
         message.openGroupServerMessageId = openGroupServerMessageId
diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift
index 2eeba82f7..00561de98 100644
--- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift
+++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift
@@ -12,8 +12,8 @@ import SignalUtilitiesKit
 import SessionUtilitiesKit
 
 public final class NotificationServiceExtension: UNNotificationServiceExtension {
-    private let dependencies: Dependencies = Dependencies()
-    private var didPerformSetup = false
+    private var dependencies: Dependencies = Dependencies()
+    private var startTime: CFTimeInterval = 0
     private var contentHandler: ((UNNotificationContent) -> Void)?
     private var request: UNNotificationRequest?
     private var hasCompleted: Atomic<Bool> = Atomic(false)
@@ -27,6 +27,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
     // MARK: Did receive a remote push notification request
     
     override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+        self.startTime = CACurrentMediaTime()
         self.contentHandler = contentHandler
         self.request = request
         
@@ -51,37 +52,23 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
             Singleton.setup(appContext: NotificationServiceExtensionContext())
         }
         
-        // Perform main setup
-        Storage.resumeDatabaseAccess(using: dependencies)
+        /// Perform main setup (create a new `Dependencies` instance each time so we don't need to worry about state from previous
+        /// notifications causing issues with new notifications
+        self.dependencies = Dependencies()
+        
         DispatchQueue.main.sync {
-            self.setUpIfNecessary() { [weak self] in
+            self.performSetup { [weak self] in
                 self?.handleNotification(notificationContent, isPerformingResetup: false)
             }
         }
     }
     
     private func handleNotification(_ notificationContent: UNMutableNotificationContent, isPerformingResetup: Bool) {
-        let userSessionId: String = getUserHexEncodedPublicKey(using: dependencies)
         let (maybeData, metadata, result) = PushNotificationAPI.processNotification(
             notificationContent: notificationContent,
             using: dependencies
         )
         
-        /// There is an annoying issue where clearing account data and creating a new account can result in the user receiving push notifications
-        /// for the new account but the NotificationServiceExtension having cached state based on the old account
-        ///
-        /// In order to avoid this we check if the account the notification was sent to matches the current users sessionId and if it doesn't (and the
-        /// notification is for a message stored in one of the users namespaces) then try to re-setup the notification extension
-        guard !metadata.namespace.isCurrentUserNamespace || metadata.accountId == userSessionId else {
-            guard !isPerformingResetup else {
-                Log.error("Received notification for an accountId that isn't the current user, resetup failed.")
-                return self.completeSilenty(handledNotification: false)
-            }
-            
-            Log.warn("Received notification for an accountId that isn't the current user, attempting to resetup.")
-            return self.forceResetup(notificationContent)
-        }
-        
         guard
             (result == .success || result == .legacySuccess),
             let data: Data = maybeData
@@ -246,31 +233,35 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
                 // If an error occurred we want to rollback the transaction (by throwing) and then handle
                 // the error outside of the database
                 let handleError = {
-                    switch error {
-                        case MessageReceiverError.noGroupKeyPair:
-                            Log.warn("Failed due to having no legacy group decryption keys.")
-                            self?.completeSilenty(handledNotification: false)
-                            
-                        case MessageReceiverError.outdatedMessage:
-                            Log.info("Ignoring notification for already seen message.")
-                            self?.completeSilenty(handledNotification: false)
-                            
-                        case NotificationError.ignorableMessage:
-                            Log.info("Ignoring message which requires no notification.")
-                            self?.completeSilenty(handledNotification: false)
-                            
-                        case MessageReceiverError.duplicateMessage, MessageReceiverError.duplicateControlMessage,
-                            MessageReceiverError.duplicateMessageNewSnode:
-                            Log.info("Ignoring duplicate message (probably received it just before going to the background).")
-                            self?.completeSilenty(handledNotification: false)
-                            
-                        case NotificationError.messageProcessing:
-                            self?.handleFailure(for: notificationContent, error: .messageProcessing)
-                            
-                        case let msgError as MessageReceiverError:
-                            self?.handleFailure(for: notificationContent, error: .messageHandling(msgError))
-                            
-                        default: self?.handleFailure(for: notificationContent, error: .other(error))
+                    // Dispatch to the next run loop to ensure we are out of the database write thread before
+                    // handling the result (and suspending the database)
+                    DispatchQueue.main.async {
+                        switch error {
+                            case MessageReceiverError.noGroupKeyPair:
+                                Log.warn("Failed due to having no legacy group decryption keys.")
+                                self?.completeSilenty(handledNotification: false)
+                                
+                            case MessageReceiverError.outdatedMessage:
+                                Log.info("Ignoring notification for already seen message.")
+                                self?.completeSilenty(handledNotification: false)
+                                
+                            case NotificationError.ignorableMessage:
+                                Log.info("Ignoring message which requires no notification.")
+                                self?.completeSilenty(handledNotification: false)
+                                
+                            case MessageReceiverError.duplicateMessage, MessageReceiverError.duplicateControlMessage,
+                                MessageReceiverError.duplicateMessageNewSnode:
+                                Log.info("Ignoring duplicate message (probably received it just before going to the background).")
+                                self?.completeSilenty(handledNotification: false)
+                                
+                            case NotificationError.messageProcessing:
+                                self?.handleFailure(for: notificationContent, error: .messageProcessing)
+                                
+                            case let msgError as MessageReceiverError:
+                                self?.handleFailure(for: notificationContent, error: .messageHandling(msgError))
+                                
+                            default: self?.handleFailure(for: notificationContent, error: .other(error))
+                        }
                     }
                 }
                 
@@ -285,18 +276,29 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
 
     // MARK: Setup
 
-    private func setUpIfNecessary(completion: @escaping () -> Void) {
-        Log.assertOnMainThread()
-
-        // The NSE will often re-use the same process, so if we're
-        // already set up we want to do nothing; we're already ready
-        // to process new messages.
-        guard !didPerformSetup else { return completion() }
-
+    private func performSetup(completion: @escaping () -> Void) {
         Log.info("Performing setup.")
-        didPerformSetup = true
 
         _ = AppVersion.shared
+        
+        // FIXME: Remove these once the database instance is fully managed via `Dependencies`
+        if AppSetup.hasRun {
+            dependencies.storage.resumeDatabaseAccess()
+            dependencies.storage.reconfigureDatabase()
+            dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() }
+            
+            // If we had already done a setup then `libSession` won't have been re-setup so
+            // we need to do so now (this ensures it has the correct user keys as well)
+            LibSession.clearMemoryState(using: dependencies)
+            dependencies.storage.read { [dependencies] db in
+                LibSession.loadState(
+                    db,
+                    userPublicKey: getUserHexEncodedPublicKey(db, using: dependencies),
+                    ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey,
+                    using: dependencies
+                )
+            }
+        }
 
         AppSetup.setupEnvironment(
             retrySetupIfDatabaseInvalid: true,
@@ -315,26 +317,39 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
                 // Setup LibSession
                 LibSession.addLogger()
             },
-            migrationsCompletion: { [weak self] result, needsConfigSync in
+            migrationsCompletion: { [weak self, dependencies] result, _ in
                 switch result {
                     case .failure(let error):
                         Log.error("Failed to complete migrations: \(error).")
                         self?.completeSilenty(handledNotification: false)
                         
                     case .success:
-                        // We should never receive a non-voip notification on an app that doesn't support
-                        // app extensions since we have to inform the service we wanted these, so in theory
-                        // this path should never occur. However, the service does have our push token
-                        // so it is possible that could change in the future. If it does, do nothing
-                        // and don't disturb the user. Messages will be processed when they open the app.
-                        guard Storage.shared[.isReadyForAppExtensions] else {
-                            Log.error("Not ready for extensions.")
-                            self?.completeSilenty(handledNotification: false)
-                            return
-                        }
-                        
                         DispatchQueue.main.async {
-                            self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync, completion: completion)
+                            // Ensure storage is actually valid
+                            guard dependencies.storage.isValid else {
+                                Log.error("Storage invalid.")
+                                self?.completeSilenty(handledNotification: false)
+                                return
+                            }
+                            
+                            // We should never receive a non-voip notification on an app that doesn't support
+                            // app extensions since we have to inform the service we wanted these, so in theory
+                            // this path should never occur. However, the service does have our push token
+                            // so it is possible that could change in the future. If it does, do nothing
+                            // and don't disturb the user. Messages will be processed when they open the app.
+                            guard dependencies.storage[.isReadyForAppExtensions] else {
+                                Log.error("Not ready for extensions.")
+                                self?.completeSilenty(handledNotification: false)
+                                return
+                            }
+                            
+                            // If the app wasn't ready then mark it as ready now
+                            if !Singleton.appReadiness.isAppReady {
+                                // Note that this does much more than set a flag; it will also run all deferred blocks.
+                                Singleton.appReadiness.setAppReady()
+                            }
+
+                            completion()
                         }
                 }
             },
@@ -342,60 +357,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
         )
     }
     
-    private func versionMigrationsDidComplete(needsConfigSync: Bool, completion: @escaping () -> Void) {
-        Log.assertOnMainThread()
-
-        // If we need a config sync then trigger it now
-        if needsConfigSync {
-            Storage.shared.write { db in
-                ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
-            }
-        }
-
-        // App isn't ready until storage is ready AND all version migrations are complete.
-        guard Storage.shared.isValid else {
-            Log.error("Storage invalid.")
-            return self.completeSilenty(handledNotification: false)
-        }
-        
-        // If the app wasn't ready then mark it as ready now
-        if !Singleton.appReadiness.isAppReady {
-            // Note that this does much more than set a flag; it will also run all deferred blocks.
-            Singleton.appReadiness.setAppReady()
-        }
-        
-        completion()
-    }
-    
-    /// It's possible for the NotificationExtension to still have some kind of cached data from the old database after it's been deleted
-    /// when a new account is created shortly after, this results in weird errors when receiving PNs for the new account
-    ///
-    /// In order to avoid this situation we check to see whether the received PN is targetting the current user and, if not, we call this
-    /// method to force a resetup of the notification extension
-    ///
-    /// **Note:** We need to reconfigure the database here because if the database was deleted it's possible for the NotificationExtension
-    /// to somehow still have some form of access to the old one
-    private func forceResetup(_ notificationContent: UNMutableNotificationContent) {
-        Storage.reconfigureDatabase()
-        LibSession.clearMemoryState(using: dependencies)
-        dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() }
-        
-        self.setUpIfNecessary() { [weak self, dependencies] in
-            // If we had already done a setup then `libSession` won't have been re-setup so
-            // we need to do so now (this ensures it has the correct user keys as well)
-            Storage.shared.read { db in
-                LibSession.loadState(
-                    db,
-                    userPublicKey: getUserHexEncodedPublicKey(db),
-                    ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey,
-                    using: dependencies
-                )
-            }
-            
-            self?.handleNotification(notificationContent, isPerformingResetup: true)
-        }
-    }
-    
     // MARK: Handle completion
     
     override public func serviceExtensionTimeWillExpire() {
@@ -416,15 +377,17 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
         else { return }
         
         let silentContent: UNMutableNotificationContent = UNMutableNotificationContent()
-        silentContent.badge = Storage.shared
-            .read { db in try Interaction.fetchUnreadCount(db) }
-            .map { NSNumber(value: $0) }
-            .defaulting(to: NSNumber(value: 0))
         
-        Log.info(handledNotification ? "Completed after handling notification." : "Completed silently.")
         if !isMainAppAndActive {
-            Storage.suspendDatabaseAccess(using: dependencies)
+            silentContent.badge = dependencies.storage
+                .read { db in try Interaction.fetchUnreadCount(db) }
+                .map { NSNumber(value: $0) }
+                .defaulting(to: NSNumber(value: 0))
+            dependencies.storage.suspendDatabaseAccess()
         }
+        
+        let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
+        Log.info(handledNotification ? "Completed after handling notification in \(.seconds(duration), unit: .ms)." : "Completed silently after \(.seconds(duration), unit: .ms).")
         Log.flush()
         
         self.contentHandler!(silentContent)
@@ -502,8 +465,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
     }
 
     private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
-        Log.error("Show generic failure message due to error: \(error).")
-        Storage.suspendDatabaseAccess(using: dependencies)
+        dependencies.storage.suspendDatabaseAccess()
+        
+        let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
+        Log.error("Show generic failure message after \(.seconds(duration), unit: .ms) due to error: \(error).")
         Log.flush()
         
         content.title = "Session"
diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift
index 259f9ef5d..52b17832a 100644
--- a/SessionShareExtension/ThreadPickerVC.swift
+++ b/SessionShareExtension/ThreadPickerVC.swift
@@ -111,7 +111,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
         // When the thread picker disappears it means the user has left the screen (this will be called
         // whether the user has sent the message or cancelled sending)
         LibSession.suspendNetworkAccess()
-        Storage.suspendDatabaseAccess(using: viewModel.dependencies)
+        viewModel.dependencies.storage.suspendDatabaseAccess()
         Log.flush()
     }
     
@@ -240,7 +240,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
         shareNavController?.dismiss(animated: true, completion: nil)
         
         ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { [dependencies = viewModel.dependencies] activityIndicator in
-            Storage.resumeDatabaseAccess(using: dependencies)
+            dependencies.storage.resumeDatabaseAccess()
             LibSession.resumeNetworkAccess()
             
             let swarmPublicKey: String = {
@@ -336,7 +336,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
                 .sinkUntilComplete(
                     receiveCompletion: { [weak self] result in
                         LibSession.suspendNetworkAccess()
-                        Storage.suspendDatabaseAccess(using: dependencies)
+                        dependencies.storage.suspendDatabaseAccess()
                         Log.flush()
                         activityIndicator.dismiss { }
                         
diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift
index 68792df55..82352e7be 100644
--- a/SessionUtilitiesKit/Database/Storage.swift
+++ b/SessionUtilitiesKit/Database/Storage.swift
@@ -91,7 +91,7 @@ open class Storage {
         /// **Note:** If we fail to get/generate the keySpec then don't bother continuing to setup the Database as it'll just be invalid,
         /// in this case the App/Extensions will have logic that checks the `isValid` flag of the database
         do {
-            var tmpKeySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
+            var tmpKeySpec: Data = try getOrGenerateDatabaseKeySpec()
             tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count)
         }
         catch { return }
@@ -108,8 +108,8 @@ open class Storage {
         config.busyMode = .timeout(Storage.writeTransactionStartTimeout)
 
         /// Load in the SQLCipher keys
-        config.prepareDatabase { db in
-            var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
+        config.prepareDatabase { [weak self] db in
+            var keySpec: Data = try self?.getOrGenerateDatabaseKeySpec() ?? { throw StorageError.invalidKeySpec }()
             defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
             
             // Use a raw key spec, where the 96 hexadecimal digits are provided
@@ -358,12 +358,12 @@ open class Storage {
         return try Singleton.keychain.data(forKey: .dbCipherKeySpec)
     }
     
-    @discardableResult private static func getOrGenerateDatabaseKeySpec() throws -> Data {
+    @discardableResult private func getOrGenerateDatabaseKeySpec() throws -> Data {
         do {
-            var keySpec: Data = try getDatabaseCipherKeySpec()
+            var keySpec: Data = try Storage.getDatabaseCipherKeySpec()
             defer { keySpec.resetBytes(in: 0..<keySpec.count) }
             
-            guard keySpec.count == kSQLCipherKeySpecLength else { throw StorageError.invalidKeySpec }
+            guard keySpec.count == Storage.kSQLCipherKeySpecLength else { throw StorageError.invalidKeySpec }
             
             return keySpec
         }
@@ -382,7 +382,7 @@ open class Storage {
                 case (_, errSecItemNotFound):
                     // No keySpec was found so we need to generate a new one
                     do {
-                        var keySpec: Data = try Randomness.generateRandomBytes(numberBytes: kSQLCipherKeySpecLength)
+                        var keySpec: Data = try Randomness.generateRandomBytes(numberBytes: Storage.kSQLCipherKeySpecLength)
                         defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
                         
                         try Singleton.keychain.set(data: keySpec, forKey: .dbCipherKeySpec)
@@ -426,40 +426,41 @@ open class Storage {
     /// The generally suggested approach is to avoid this entirely by not storing the database in an AppGroup folder and sharing it
     /// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the
     /// database and other files into the App folder
-    public static func suspendDatabaseAccess(using dependencies: Dependencies) {
-        guard !dependencies.storage.isSuspended else { return }
+    public func suspendDatabaseAccess() {
+        guard !isSuspended else { return }
         
-        dependencies.storage.isSuspended = true
+        isSuspended = true
         Log.info("[Storage] Database access suspended.")
         
         /// Interrupt any open transactions (if this function is called then we are expecting that all processes have finished running
         /// and don't actually want any more transactions to occur)
-        dependencies.storage.dbWriter?.interrupt()
+        dbWriter?.interrupt()
     }
     
     /// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()`
     /// above for more information
-    public static func resumeDatabaseAccess(using dependencies: Dependencies) {
-        guard dependencies.storage.isSuspended else { return }
-        dependencies.storage.isSuspended = false
+    public func resumeDatabaseAccess() {
+        guard isSuspended else { return }
+
+        isSuspended = false
         Log.info("[Storage] Database access resumed.")
     }
     
-    public static func resetAllStorage() {
-        Storage.shared.isValid = false
+    public func resetAllStorage() {
+        isValid = false
         Storage.internalHasCreatedValidInstance.mutate { $0 = false }
-        Storage.shared.migrationsCompleted.mutate { $0 = false }
-        Storage.shared.dbWriter = nil
+        migrationsCompleted.mutate { $0 = false }
+        dbWriter = nil
         
         deleteDatabaseFiles()
         do { try deleteDbKeys() } catch { Log.warn("Failed to delete database keys.") }
     }
     
-    public static func reconfigureDatabase() {
-        Storage.shared.configureDatabase()
+    public func reconfigureDatabase() {
+        configureDatabase()
     }
     
-    public static func resetForCleanMigration() {
+    public func resetForCleanMigration() {
         // Clear existing content
         resetAllStorage()
         
@@ -467,13 +468,13 @@ open class Storage {
         reconfigureDatabase()
     }
     
-    private static func deleteDatabaseFiles() {
-        do { try FileSystem.deleteFile(at: databasePath) } catch { Log.warn("Failed to delete database.") }
-        do { try FileSystem.deleteFile(at: databasePathShm) } catch { Log.warn("Failed to delete database-shm.") }
-        do { try FileSystem.deleteFile(at: databasePathWal) } catch { Log.warn("Failed to delete database-wal.") }
+    private func deleteDatabaseFiles() {
+        do { try FileSystem.deleteFile(at: Storage.databasePath) } catch { Log.warn("Failed to delete database.") }
+        do { try FileSystem.deleteFile(at: Storage.databasePathShm) } catch { Log.warn("Failed to delete database-shm.") }
+        do { try FileSystem.deleteFile(at: Storage.databasePathWal) } catch { Log.warn("Failed to delete database-wal.") }
     }
     
-    private static func deleteDbKeys() throws {
+    private func deleteDbKeys() throws {
         try Singleton.keychain.remove(key: .dbCipherKeySpec)
     }
     
@@ -734,7 +735,7 @@ public extension ValueObservation {
 #if DEBUG
 public extension Storage {
     func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) {
-        var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
+        var keySpec: Data = try getOrGenerateDatabaseKeySpec()
         defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
         
         guard var passwordData: Data = password.data(using: .utf8) else { throw StorageError.generic }
diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift
index b936ec0e6..23cbb1320 100644
--- a/SessionUtilitiesKit/General/Logging.swift
+++ b/SessionUtilitiesKit/General/Logging.swift
@@ -446,7 +446,7 @@ public class Logger {
                         }
                 }
                 catch {
-                    self?.completeResumeLogging(error: "Unable to write extension logs to current log file")
+                    self?.completeResumeLogging(error: "Unable to write extension logs to current log file due to error: \(error)")
                     return
                 }
                 
diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift
index c1bf1829c..e5227a968 100644
--- a/SignalUtilitiesKit/Utilities/AppSetup.swift
+++ b/SignalUtilitiesKit/Utilities/AppSetup.swift
@@ -8,7 +8,8 @@ import SessionUIKit
 import SessionSnodeKit
 
 public enum AppSetup {
-    private static let hasRun: Atomic<Bool> = Atomic(false)
+    private static let _hasRun: Atomic<Bool> = Atomic(false)
+    public static var hasRun: Bool { _hasRun.wrappedValue }
     
     public static func setupEnvironment(
         retrySetupIfDatabaseInvalid: Bool = false,
@@ -18,13 +19,13 @@ public enum AppSetup {
         using dependencies: Dependencies
     ) {
         // If we've already run the app setup then only continue under certain circumstances
-        guard !AppSetup.hasRun.wrappedValue else {
-            let storageIsValid: Bool = Storage.shared.isValid
+        guard !AppSetup._hasRun.wrappedValue else {
+            let storageIsValid: Bool = dependencies.storage.isValid
             
             switch (retrySetupIfDatabaseInvalid, storageIsValid) {
                 case (true, false):
-                    Storage.reconfigureDatabase()
-                    AppSetup.hasRun.mutate { $0 = false }
+                    dependencies.storage.reconfigureDatabase()
+                    AppSetup._hasRun.mutate { $0 = false }
                     AppSetup.setupEnvironment(
                         retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop
                         appSpecificBlock: appSpecificBlock,
@@ -42,7 +43,7 @@ public enum AppSetup {
             return
         }
         
-        AppSetup.hasRun.mutate { $0 = true }
+        AppSetup._hasRun.mutate { $0 = true }
         
         var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function)
         
@@ -91,7 +92,7 @@ public enum AppSetup {
     ) {
         var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function))
         
-        Storage.shared.perform(
+        dependencies.storage.perform(
             migrationTargets: [
                 SNUtilitiesKit.self,
                 SNSnodeKit.self,