diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..61d3cc5840af204a0b1ac7d0f8fc6ea574d0bce0
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1400"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "BackupFeature"
+               BuildableName = "BackupFeature"
+               BlueprintName = "BackupFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "BackupFeatureTests"
+               BuildableName = "BackupFeatureTests"
+               BlueprintName = "BackupFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "BackupFeature"
+            BuildableName = "BackupFeature"
+            BlueprintName = "BackupFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index b310549f615363a0f38348b5c79f2d158d00c0e2..4668d629ddf3b295ce57fe54329d28101e94c7ff 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -15,6 +15,7 @@ let package = Package(
   products: [
     .library(name: "AppCore", targets: ["AppCore"]),
     .library(name: "AppFeature", targets: ["AppFeature"]),
+    .library(name: "BackupFeature", targets: ["BackupFeature"]),
     .library(name: "ChatFeature", targets: ["ChatFeature"]),
     .library(name: "CheckContactAuthFeature", targets: ["CheckContactAuthFeature"]),
     .library(name: "ConfirmRequestFeature", targets: ["ConfirmRequestFeature"]),
@@ -83,6 +84,7 @@ let package = Package(
       name: "AppFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "BackupFeature"),
         .target(name: "ChatFeature"),
         .target(name: "CheckContactAuthFeature"),
         .target(name: "ConfirmRequestFeature"),
@@ -112,6 +114,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "BackupFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "BackupFeatureTests",
+      dependencies: [
+        .target(name: "BackupFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "ChatFeature",
       dependencies: [
@@ -215,6 +234,7 @@ let package = Package(
       name: "HomeFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "BackupFeature"),
         .target(name: "ContactsFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "UserSearchFeature"),
diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
index aa97ea7d2a0f1ce2fded6d13e874f8f8caf21769..b50f1c107c6e3f6e79c5c4c22e64b4368732de84 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -49,6 +49,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "BackupFeatureTests"
+               BuildableName = "BackupFeatureTests"
+               BlueprintName = "BackupFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 24e5bd8910a267354a1a608579e5ddb31d1f4cc0..3cf2cb0b065eda90aeff16cf1ce76f14297fd012 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -1,4 +1,5 @@
 import AppCore
+import BackupFeature
 import ChatFeature
 import CheckContactAuthFeature
 import ConfirmRequestFeature
@@ -27,6 +28,7 @@ extension AppEnvironment {
       handleConfirm: .live(db: dbManager.getDB),
       handleReset: .live(db: dbManager.getDB)
     )
+    let backupStorage = BackupStorage.onDisk()
     let mainQueue = DispatchQueue.main.eraseToAnyScheduler()
     let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler()
 
@@ -90,6 +92,7 @@ extension AppEnvironment {
         messenger: messenger,
         db: dbManager.getDB
       ),
+      backupStorage: backupStorage,
       log: .live(),
       mainQueue: mainQueue,
       bgQueue: bgQueue,
@@ -149,6 +152,15 @@ extension AppEnvironment {
               bgQueue: bgQueue,
               contact: { contactEnvironment }
             )
+          },
+          backup: {
+            BackupEnvironment(
+              messenger: messenger,
+              db: dbManager.getDB,
+              backupStorage: backupStorage,
+              mainQueue: mainQueue,
+              bgQueue: bgQueue
+            )
           }
         )
       }
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
index caae3368d778873e89af41f51a8a9ecce581866c..07be9948b695eb3bb8ecda07fb674ca77e8e7141 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
@@ -50,6 +50,7 @@ struct AppEnvironment {
   var messenger: Messenger
   var authHandler: AuthCallbackHandler
   var messageListener: MessageListenerHandler
+  var backupStorage: BackupStorage
   var log: Logger
   var mainQueue: AnySchedulerOf<DispatchQueue>
   var bgQueue: AnySchedulerOf<DispatchQueue>
@@ -64,6 +65,7 @@ extension AppEnvironment {
     messenger: .unimplemented,
     authHandler: .unimplemented,
     messageListener: .unimplemented,
+    backupStorage: .unimplemented,
     log: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
@@ -94,6 +96,9 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment>
         cancellables.append(env.messageListener(onError: { error in
           env.log(.error(error as NSError))
         }))
+        cancellables.append(env.messenger.registerBackupCallback(.init { data in
+          try? env.backupStorage.store(data)
+        }))
 
         let isLoaded = env.messenger.isLoaded()
         let isCreated = env.messenger.isCreated()
diff --git a/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2bd95f770ff5ce1be9e808284d1d50f3d111af1d
--- /dev/null
+++ b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+
+extension AlertState where Action == BackupAction {
+  public static func error(_ error: Error) -> AlertState<BackupAction> {
+    AlertState(
+      title: TextState("Error"),
+      message: TextState(error.localizedDescription)
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e1ce8fb201188ac101249d070dc96a75e4ca481a
--- /dev/null
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
@@ -0,0 +1,232 @@
+import AppCore
+import Combine
+import ComposableArchitecture
+import Foundation
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct BackupState: Equatable {
+  public enum Error: String, Swift.Error, Equatable {
+    case dbContactNotFound
+    case dbContactUsernameMissing
+  }
+
+  public init(
+    isRunning: Bool = false,
+    isStarting: Bool = false,
+    isResuming: Bool = false,
+    isStopping: Bool = false,
+    backup: BackupStorage.Backup? = nil,
+    alert: AlertState<BackupAction>? = nil,
+    passphrase: String = "",
+    isExporting: Bool = false,
+    exportData: Data? = nil
+  ) {
+    self.isRunning = isRunning
+    self.isStarting = isStarting
+    self.isResuming = isResuming
+    self.isStopping = isStopping
+    self.backup = backup
+    self.alert = alert
+    self.passphrase = passphrase
+    self.isExporting = isExporting
+    self.exportData = exportData
+  }
+
+  public var isRunning: Bool
+  public var isStarting: Bool
+  public var isResuming: Bool
+  public var isStopping: Bool
+  public var backup: BackupStorage.Backup?
+  public var alert: AlertState<BackupAction>?
+  @BindableState public var passphrase: String
+  @BindableState public var isExporting: Bool
+  public var exportData: Data?
+}
+
+public enum BackupAction: Equatable, BindableAction {
+  case task
+  case cancelTask
+  case startTapped
+  case resumeTapped
+  case stopTapped
+  case exportTapped
+  case alertDismissed
+  case backupUpdated(BackupStorage.Backup?)
+  case didStart(failure: NSError?)
+  case didResume(failure: NSError?)
+  case didStop(failure: NSError?)
+  case didExport(failure: NSError?)
+  case binding(BindingAction<BackupState>)
+}
+
+public struct BackupEnvironment {
+  public init(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    backupStorage: BackupStorage,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.db = db
+    self.backupStorage = backupStorage
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var db: DBManagerGetDB
+  public var backupStorage: BackupStorage
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
+}
+
+#if DEBUG
+extension BackupEnvironment {
+  public static let unimplemented = BackupEnvironment(
+    messenger: .unimplemented,
+    db: .unimplemented,
+    backupStorage: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
+}
+#endif
+
+public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment>
+{ state, action, env in
+  enum TaskEffectId {}
+
+  switch action {
+  case .task:
+    state.isRunning = env.messenger.isBackupRunning()
+    return Effect.run { subscriber in
+      let cancellable = env.backupStorage.observe { backup in
+        subscriber.send(.backupUpdated(backup))
+      }
+      return AnyCancellable { cancellable.cancel() }
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+    .cancellable(id: TaskEffectId.self, cancelInFlight: true)
+
+  case .cancelTask:
+    return .cancel(id: TaskEffectId.self)
+
+  case .startTapped:
+    state.isStarting = true
+    return Effect.run { [state] subscriber in
+      do {
+        let e2e: E2E = try env.messenger.e2e.tryGet()
+        let contactID = try e2e.getContact().getId()
+        let db = try env.db()
+        let query = XXModels.Contact.Query(id: [contactID])
+        guard let contact = try db.fetchContacts(query).first else {
+          throw BackupState.Error.dbContactNotFound
+        }
+        guard let username = contact.username else {
+          throw BackupState.Error.dbContactUsernameMissing
+        }
+        try env.messenger.startBackup(
+          password: state.passphrase,
+          params: BackupParams(username: username)
+        )
+        subscriber.send(.didStart(failure: nil))
+      } catch {
+        subscriber.send(.didStart(failure: error as NSError))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .resumeTapped:
+    state.isResuming = true
+    return Effect.run { subscriber in
+      do {
+        try env.messenger.resumeBackup()
+        subscriber.send(.didResume(failure: nil))
+      } catch {
+        subscriber.send(.didResume(failure: error as NSError))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .stopTapped:
+    state.isStopping = true
+    return Effect.run { subscriber in
+      do {
+        try env.messenger.stopBackup()
+        try env.backupStorage.remove()
+        subscriber.send(.didStop(failure: nil))
+      } catch {
+        subscriber.send(.didStop(failure: error as NSError))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .exportTapped:
+    state.isExporting = true
+    state.exportData = state.backup?.data
+    return .none
+
+  case .alertDismissed:
+    state.alert = nil
+    return .none
+
+  case .backupUpdated(let backup):
+    state.backup = backup
+    return .none
+
+  case .didStart(let failure):
+    state.isRunning = env.messenger.isBackupRunning()
+    state.isStarting = false
+    if let failure {
+      state.alert = .error(failure)
+    } else {
+      state.passphrase = ""
+    }
+    return .none
+
+  case .didResume(let failure):
+    state.isRunning = env.messenger.isBackupRunning()
+    state.isResuming = false
+    if let failure {
+      state.alert = .error(failure)
+    }
+    return .none
+
+  case .didStop(let failure):
+    state.isRunning = env.messenger.isBackupRunning()
+    state.isStopping = false
+    if let failure {
+      state.alert = .error(failure)
+    }
+    return .none
+
+  case .didExport(let failure):
+    state.isExporting = false
+    state.exportData = nil
+    if let failure {
+      state.alert = .error(failure)
+    }
+    return .none
+
+  case .binding(_):
+    return .none
+  }
+}
+.binding()
diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..54fc6567cd07eac6c5649fa5a35e6b28a91fe3eb
--- /dev/null
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
@@ -0,0 +1,241 @@
+import ComposableArchitecture
+import SwiftUI
+import UniformTypeIdentifiers
+
+public struct BackupView: View {
+  public init(store: Store<BackupState, BackupAction>) {
+    self.store = store
+  }
+
+  let store: Store<BackupState, BackupAction>
+
+  struct ViewState: Equatable {
+    struct Backup: Equatable {
+      var date: Date
+      var size: Int
+    }
+
+    init(state: BackupState) {
+      isRunning = state.isRunning
+      isStarting = state.isStarting
+      isResuming = state.isResuming
+      isStopping = state.isStopping
+      backup = state.backup.map { backup in
+        Backup(date: backup.date, size: backup.data.count)
+      }
+      passphrase = state.passphrase
+      isExporting = state.isExporting
+      exportData = state.exportData
+    }
+
+    var isRunning: Bool
+    var isStarting: Bool
+    var isResuming: Bool
+    var isStopping: Bool
+    var isLoading: Bool { isStarting || isResuming || isStopping }
+    var backup: Backup?
+    var passphrase: String
+    var isExporting: Bool
+    var exportData: Data?
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+        Group {
+          if viewStore.isRunning || viewStore.backup != nil {
+            backupSection(viewStore)
+          }
+          if !viewStore.isRunning {
+            newBackupSection(viewStore)
+          }
+        }
+        .disabled(viewStore.isLoading)
+        .alert(
+          store.scope(state: \.alert),
+          dismiss: .alertDismissed
+        )
+      }
+      .navigationTitle("Backup")
+      .task {
+        await viewStore.send(.task).finish()
+      }
+    }
+  }
+
+  @ViewBuilder func newBackupSection(
+    _ viewStore: ViewStore<ViewState, BackupAction>
+  ) -> some View {
+    Section {
+      SecureField(
+        text: viewStore.binding(
+          get: \.passphrase,
+          send: { .set(\.$passphrase, $0) }
+        ),
+        prompt: Text("Backup passphrase"),
+        label: { Text("Backup passphrase") }
+      )
+      Button {
+        viewStore.send(.startTapped)
+      } label: {
+        HStack {
+          Text("Start")
+          Spacer()
+          if viewStore.isStarting {
+            ProgressView()
+          } else {
+            Image(systemName: "play.fill")
+          }
+        }
+      }
+    } header: {
+      Text("New backup")
+    }
+  }
+
+  @ViewBuilder func backupSection(
+    _ viewStore: ViewStore<ViewState, BackupAction>
+  ) -> some View {
+    Section {
+      backupView(viewStore)
+      stopView(viewStore)
+      resumeView(viewStore)
+    } header: {
+      Text("Backup")
+    }
+  }
+
+  @ViewBuilder func backupView(
+    _ viewStore: ViewStore<ViewState, BackupAction>
+  ) -> some View {
+    if let backup = viewStore.backup {
+      HStack {
+        Text("Date")
+        Spacer()
+        Text(backup.date.formatted())
+      }
+      HStack {
+        Text("Size")
+        Spacer()
+        Text(format(bytesCount: backup.size))
+      }
+      Button {
+        viewStore.send(.exportTapped)
+      } label: {
+        HStack {
+          Text("Export")
+          Spacer()
+          if viewStore.isExporting {
+            ProgressView()
+          } else {
+            Image(systemName: "square.and.arrow.up")
+          }
+        }
+      }
+      .disabled(viewStore.isExporting)
+      .fileExporter(
+        isPresented: viewStore.binding(
+          get: \.isExporting,
+          send: { .set(\.$isExporting, $0) }
+        ),
+        document: viewStore.exportData.map(ExportedDocument.init(data:)),
+        contentType: .data,
+        defaultFilename: "backup.xxm",
+        onCompletion: { result in
+          switch result {
+          case .success(_):
+            viewStore.send(.didExport(failure: nil))
+          case .failure(let error):
+            viewStore.send(.didExport(failure: error as NSError?))
+          }
+        }
+      )
+    } else {
+      Text("No backup")
+    }
+  }
+
+  @ViewBuilder func stopView(
+    _ viewStore: ViewStore<ViewState, BackupAction>
+  ) -> some View {
+    if viewStore.isRunning {
+      Button {
+        viewStore.send(.stopTapped)
+      } label: {
+        HStack {
+          Text("Stop")
+          Spacer()
+          if viewStore.isStopping {
+            ProgressView()
+          } else {
+            Image(systemName: "stop.fill")
+          }
+        }
+      }
+    }
+  }
+
+  @ViewBuilder func resumeView(
+    _ viewStore: ViewStore<ViewState, BackupAction>
+  ) -> some View {
+    if !viewStore.isRunning, viewStore.backup != nil {
+      Button {
+        viewStore.send(.resumeTapped)
+      } label: {
+        HStack {
+          Text("Resume")
+          Spacer()
+          if viewStore.isResuming {
+            ProgressView()
+          } else {
+            Image(systemName: "playpause.fill")
+          }
+        }
+      }
+    }
+  }
+
+  func format(bytesCount bytes: Int) -> String {
+    let formatter = ByteCountFormatter()
+    formatter.allowedUnits = [.useMB, .useKB]
+    formatter.countStyle = .binary
+    return formatter.string(fromByteCount: Int64(bytes))
+  }
+}
+
+private struct ExportedDocument: FileDocument {
+  enum Error: Swift.Error {
+    case notAvailable
+  }
+
+  static var readableContentTypes: [UTType] = []
+  static var writableContentTypes: [UTType] = [.data]
+
+  var data: Data
+
+  init(data: Data) {
+    self.data = data
+  }
+
+  init(configuration: ReadConfiguration) throws {
+    throw Error.notAvailable
+  }
+
+  func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
+    FileWrapper(regularFileWithContents: data)
+  }
+}
+
+#if DEBUG
+public struct BackupView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      BackupView(store: Store(
+        initialState: BackupState(),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
index 068cdb4dd77598fede025166b1a5e7505f1a7ba7..87a516000a941d139add614d91b8276da3d5acab 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
@@ -1,4 +1,5 @@
 import AppCore
+import BackupFeature
 import Combine
 import ComposableArchitecture
 import ComposablePresentation
@@ -20,7 +21,8 @@ public struct HomeState: Equatable {
     alert: AlertState<HomeAction>? = nil,
     register: RegisterState? = nil,
     contacts: ContactsState? = nil,
-    userSearch: UserSearchState? = nil
+    userSearch: UserSearchState? = nil,
+    backup: BackupState? = nil
   ) {
     self.failure = failure
     self.isNetworkHealthy = isNetworkHealthy
@@ -29,6 +31,7 @@ public struct HomeState: Equatable {
     self.register = register
     self.contacts = contacts
     self.userSearch = userSearch
+    self.backup = backup
   }
 
   public var failure: String?
@@ -39,6 +42,7 @@ public struct HomeState: Equatable {
   public var register: RegisterState?
   public var contacts: ContactsState?
   public var userSearch: UserSearchState?
+  public var backup: BackupState?
 }
 
 public enum HomeAction: Equatable {
@@ -72,9 +76,12 @@ public enum HomeAction: Equatable {
   case didDismissUserSearch
   case contactsButtonTapped
   case didDismissContacts
+  case backupButtonTapped
+  case didDismissBackup
   case register(RegisterAction)
   case contacts(ContactsAction)
   case userSearch(UserSearchAction)
+  case backup(BackupAction)
 }
 
 public struct HomeEnvironment {
@@ -85,7 +92,8 @@ public struct HomeEnvironment {
     bgQueue: AnySchedulerOf<DispatchQueue>,
     register: @escaping () -> RegisterEnvironment,
     contacts: @escaping () -> ContactsEnvironment,
-    userSearch: @escaping () -> UserSearchEnvironment
+    userSearch: @escaping () -> UserSearchEnvironment,
+    backup: @escaping () -> BackupEnvironment
   ) {
     self.messenger = messenger
     self.dbManager = dbManager
@@ -94,6 +102,7 @@ public struct HomeEnvironment {
     self.register = register
     self.contacts = contacts
     self.userSearch = userSearch
+    self.backup = backup
   }
 
   public var messenger: Messenger
@@ -103,6 +112,7 @@ public struct HomeEnvironment {
   public var register: () -> RegisterEnvironment
   public var contacts: () -> ContactsEnvironment
   public var userSearch: () -> UserSearchEnvironment
+  public var backup: () -> BackupEnvironment
 }
 
 extension HomeEnvironment {
@@ -113,7 +123,8 @@ extension HomeEnvironment {
     bgQueue: .unimplemented,
     register: { .unimplemented },
     contacts: { .unimplemented },
-    userSearch: { .unimplemented }
+    userSearch: { .unimplemented },
+    backup: { .unimplemented }
   )
 }
 
@@ -145,6 +156,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
             try env.messenger.logIn()
           }
 
+          if !env.messenger.isBackupRunning() {
+            try? env.messenger.resumeBackup()
+          }
+
           return .success(.messenger(.didStartRegistered))
         } catch {
           return .success(.messenger(.failure(error as NSError)))
@@ -267,7 +282,15 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
     state.register = nil
     return Effect(value: .messenger(.start))
 
-  case .register(_), .contacts(_), .userSearch(_):
+  case .backupButtonTapped:
+    state.backup = BackupState()
+    return .none
+
+  case .didDismissBackup:
+    state.backup = nil
+    return .none
+
+  case .register(_), .contacts(_), .userSearch(_), .backup(_):
     return .none
   }
 }
@@ -292,3 +315,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
   action: /HomeAction.userSearch,
   environment: { $0.userSearch() }
 )
+.presenting(
+  backupReducer,
+  state: .keyPath(\.backup),
+  id: .notNil(),
+  action: /HomeAction.backup,
+  environment: { $0.backup() }
+)
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
index 03907b18d08fa6a5e39502060ec3f63b44773404..95bde09c14e3f2674768ccfb1aefc8eb3e594891 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
@@ -1,3 +1,4 @@
+import BackupFeature
 import ComposableArchitecture
 import ComposablePresentation
 import ContactsFeature
@@ -111,6 +112,16 @@ public struct HomeView: View {
           }
 
           Section {
+            Button {
+              viewStore.send(.backupButtonTapped)
+            } label: {
+              HStack {
+                Text("Backup")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
+
             Button(role: .destructive) {
               viewStore.send(.deleteAccount(.buttonTapped))
             } label: {
@@ -152,6 +163,16 @@ public struct HomeView: View {
           },
           destination: UserSearchView.init(store:)
         ))
+        .background(NavigationLinkWithStore(
+          store.scope(
+            state: \.backup,
+            action: HomeAction.backup
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissBackup)
+          },
+          destination: BackupView.init(store:)
+        ))
       }
       .navigationViewStyle(.stack)
       .task { viewStore.send(.messenger(.start)) }
diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
index 5bda4eb3f27c96014321e14766017b81a9f05f4e..098a026c2c665c803b9b1ef00d4c6dbbe9250f0a 100644
--- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -10,7 +10,7 @@ import XXClient
 
 final class AppFeatureTests: XCTestCase {
   func testStartWithoutMessengerCreated() {
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(),
@@ -34,24 +34,29 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.start)
 
     store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
       $0.screen = .welcome(WelcomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didMakeDB,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
     ])
 
     store.send(.stop)
   }
 
   func testStartWithMessengerCreated() {
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(),
@@ -78,17 +83,22 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.start)
 
     store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didMakeDB,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
 
@@ -96,7 +106,7 @@ final class AppFeatureTests: XCTestCase {
   }
 
   func testWelcomeFinished() {
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(
@@ -122,7 +132,12 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.welcome(.finished)) {
       $0.screen = .loading
     }
@@ -130,10 +145,10 @@ final class AppFeatureTests: XCTestCase {
     store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
 
@@ -141,7 +156,7 @@ final class AppFeatureTests: XCTestCase {
   }
 
   func testRestoreFinished() {
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(
@@ -167,7 +182,12 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.restore(.finished)) {
       $0.screen = .loading
     }
@@ -175,10 +195,10 @@ final class AppFeatureTests: XCTestCase {
     store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
 
@@ -186,7 +206,7 @@ final class AppFeatureTests: XCTestCase {
   }
 
   func testHomeDidDeleteAccount() {
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(
@@ -209,7 +229,12 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.home(.deleteAccount(.success))) {
       $0.screen = .loading
     }
@@ -217,10 +242,10 @@ final class AppFeatureTests: XCTestCase {
     store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
       $0.screen = .welcome(WelcomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
     ])
 
     store.send(.stop)
@@ -284,7 +309,7 @@ final class AppFeatureTests: XCTestCase {
     struct Failure: Error {}
     let error = Failure()
 
-    var actions: [Action] = []
+    var actions: [Action]!
 
     let store = TestStore(
       initialState: AppState(),
@@ -306,7 +331,12 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.messenger.registerBackupCallback.run = { _ in
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {}
+    }
 
+    actions = []
     store.send(.start)
 
     store.receive(.set(\.$screen, .failure(error.localizedDescription))) {
@@ -316,15 +346,17 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
     ])
 
     store.send(.stop)
   }
 
   func testStartHandlersAndListeners() {
-    var actions: [Action] = []
+    var actions: [Action]!
     var authHandlerOnError: [AuthCallbackHandler.OnError] = []
     var messageListenerOnError: [MessageListenerHandler.OnError] = []
+    var backupCallback: [UpdateBackupFunc] = []
 
     let store = TestStore(
       initialState: AppState(),
@@ -351,22 +383,33 @@ final class AppFeatureTests: XCTestCase {
         actions.append(.didCancelMessageListener)
       }
     }
+    store.environment.messenger.registerBackupCallback.run = { callback in
+      backupCallback.append(callback)
+      actions.append(.didRegisterBackupCallback)
+      return Cancellable {
+        actions.append(.didCancelBackupCallback)
+      }
+    }
     store.environment.log.run = { msg, _, _, _ in
       actions.append(.didLog(msg))
     }
+    store.environment.backupStorage.store = { data in
+      actions.append(.didStoreBackup(data))
+    }
 
+    actions = []
     store.send(.start)
 
     store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
     ])
-    actions = []
 
+    actions = []
     store.send(.start) {
       $0.screen = .loading
     }
@@ -374,15 +417,16 @@ final class AppFeatureTests: XCTestCase {
     store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
-
     XCTAssertNoDifference(actions, [
       .didCancelAuthHandler,
       .didCancelMessageListener,
+      .didCancelBackupCallback,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didRegisterBackupCallback,
     ])
-    actions = []
 
+    actions = []
     struct AuthError: Error {}
     let authError = AuthError()
     authHandlerOnError.first?(authError)
@@ -390,8 +434,8 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didLog(.error(authError as NSError))
     ])
-    actions = []
 
+    actions = []
     struct MessageError: Error {}
     let messageError = MessageError()
     messageListenerOnError.first?(messageError)
@@ -399,13 +443,22 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didLog(.error(messageError as NSError))
     ])
+
     actions = []
+    let backupData = "backup".data(using: .utf8)!
+    backupCallback.first?.handle(backupData)
 
+    XCTAssertNoDifference(actions, [
+      .didStoreBackup(backupData),
+    ])
+
+    actions = []
     store.send(.stop)
 
     XCTAssertNoDifference(actions, [
       .didCancelAuthHandler,
       .didCancelMessageListener,
+      .didCancelBackupCallback,
     ])
   }
 }
@@ -414,8 +467,11 @@ private enum Action: Equatable {
   case didMakeDB
   case didStartAuthHandler
   case didStartMessageListener
+  case didRegisterBackupCallback
   case didLoadMessenger
   case didCancelAuthHandler
   case didCancelMessageListener
+  case didCancelBackupCallback
   case didLog(Logger.Message)
+  case didStoreBackup(Data)
 }
diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ebb1510bc0c4f8a602db808931fe2c66ee5d3589
--- /dev/null
+++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
@@ -0,0 +1,452 @@
+import ComposableArchitecture
+import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import BackupFeature
+
+final class BackupFeatureTests: XCTestCase {
+  func testTask() {
+    var isBackupRunning: [Bool] = [false]
+    var observers: [UUID: BackupStorage.Observer] = [:]
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.backupStorage.observe = {
+      let id = UUID()
+      observers[id] = $0
+      return Cancellable { observers[id] = nil }
+    }
+
+    store.send(.task)
+
+    XCTAssertNoDifference(observers.count, 1)
+
+    let backup = BackupStorage.Backup(
+      date: .init(timeIntervalSince1970: 1),
+      data: "backup".data(using: .utf8)!
+    )
+    observers.values.forEach { $0(backup) }
+
+    store.receive(.backupUpdated(backup)) {
+      $0.backup = backup
+    }
+
+    observers.values.forEach { $0(nil) }
+
+    store.receive(.backupUpdated(nil)) {
+      $0.backup = nil
+    }
+
+    store.send(.cancelTask)
+
+    XCTAssertNoDifference(observers.count, 0)
+  }
+
+  func testStartBackup() {
+    var actions: [Action]!
+    var isBackupRunning: [Bool] = [true]
+    let contactID = "contact-id".data(using: .utf8)!
+    let dbContact = XXModels.Contact(
+      id: contactID,
+      username: "db-contact-username"
+    )
+    let passphrase = "backup-password"
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.startBackup.run = { passphrase, params in
+      actions.append(.didStartBackup(passphrase: passphrase, params: params))
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.fetchContacts.run = { _ in return [dbContact] }
+      return db
+    }
+
+    actions = []
+    store.send(.set(\.$passphrase, passphrase)) {
+      $0.passphrase = passphrase
+    }
+
+    XCTAssertNoDifference(actions, [])
+
+    actions = []
+    store.send(.startTapped) {
+      $0.isStarting = true
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didStartBackup(
+        passphrase: passphrase,
+        params: .init(username: dbContact.username!)
+      )
+    ])
+
+    store.receive(.didStart(failure: nil)) {
+      $0.isRunning = true
+      $0.isStarting = false
+      $0.passphrase = ""
+    }
+  }
+
+  func testStartBackupWithoutDbContact() {
+    var isBackupRunning: [Bool] = [false]
+    let contactID = "contact-id".data(using: .utf8)!
+
+    let store = TestStore(
+      initialState: BackupState(
+        passphrase: "1234"
+      ),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.fetchContacts.run = { _ in [] }
+      return db
+    }
+
+    store.send(.startTapped) {
+      $0.isStarting = true
+    }
+
+    let failure = BackupState.Error.dbContactNotFound
+    store.receive(.didStart(failure: failure as NSError)) {
+      $0.isRunning = false
+      $0.isStarting = false
+      $0.alert = .error(failure)
+    }
+  }
+
+  func testStartBackupWithoutDbContactUsername() {
+    var isBackupRunning: [Bool] = [false]
+    let contactID = "contact-id".data(using: .utf8)!
+    let dbContact = XXModels.Contact(
+      id: contactID,
+      username: nil
+    )
+
+    let store = TestStore(
+      initialState: BackupState(
+        passphrase: "1234"
+      ),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.fetchContacts.run = { _ in [dbContact] }
+      return db
+    }
+
+    store.send(.startTapped) {
+      $0.isStarting = true
+    }
+
+    let failure = BackupState.Error.dbContactUsernameMissing
+    store.receive(.didStart(failure: failure as NSError)) {
+      $0.isRunning = false
+      $0.isStarting = false
+      $0.alert = .error(failure)
+    }
+  }
+
+  func testStartBackupFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+    var isBackupRunning: [Bool] = [false]
+    let contactID = "contact-id".data(using: .utf8)!
+    let dbContact = XXModels.Contact(
+      id: contactID,
+      username: "db-contact-username"
+    )
+
+    let store = TestStore(
+      initialState: BackupState(
+        passphrase: "1234"
+      ),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.startBackup.run = { _, _ in
+      throw failure
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.fetchContacts.run = { _ in return [dbContact] }
+      return db
+    }
+
+    store.send(.startTapped) {
+      $0.isStarting = true
+    }
+
+    store.receive(.didStart(failure: failure as NSError)) {
+      $0.isRunning = false
+      $0.isStarting = false
+      $0.alert = .error(failure)
+    }
+  }
+
+  func testResumeBackup() {
+    var actions: [Action]!
+    var isBackupRunning: [Bool] = [true]
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.resumeBackup.run = {
+      actions.append(.didResumeBackup)
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+
+    actions = []
+    store.send(.resumeTapped) {
+      $0.isResuming = true
+    }
+
+    XCTAssertNoDifference(actions, [.didResumeBackup])
+
+    actions = []
+    store.receive(.didResume(failure: nil)) {
+      $0.isRunning = true
+      $0.isResuming = false
+    }
+
+    XCTAssertNoDifference(actions, [])
+  }
+
+  func testResumeBackupFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+    var isBackupRunning: [Bool] = [false]
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.resumeBackup.run = {
+      throw failure
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+
+    store.send(.resumeTapped) {
+      $0.isResuming = true
+    }
+
+    store.receive(.didResume(failure: failure as NSError)) {
+      $0.isRunning = false
+      $0.isResuming = false
+      $0.alert = .error(failure)
+    }
+  }
+
+  func testStopBackup() {
+    var actions: [Action]!
+    var isBackupRunning: [Bool] = [false]
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.stopBackup.run = {
+      actions.append(.didStopBackup)
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+    store.environment.backupStorage.remove = {
+      actions.append(.didRemoveBackup)
+    }
+
+    actions = []
+    store.send(.stopTapped) {
+      $0.isStopping = true
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didStopBackup,
+      .didRemoveBackup,
+    ])
+
+    actions = []
+    store.receive(.didStop(failure: nil)) {
+      $0.isRunning = false
+      $0.isStopping = false
+    }
+
+    XCTAssertNoDifference(actions, [])
+  }
+
+  func testStopBackupFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+    var isBackupRunning: [Bool] = [true]
+
+    let store = TestStore(
+      initialState: BackupState(),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.stopBackup.run = {
+      throw failure
+    }
+    store.environment.messenger.isBackupRunning.run = {
+      isBackupRunning.removeFirst()
+    }
+
+    store.send(.stopTapped) {
+      $0.isStopping = true
+    }
+
+    store.receive(.didStop(failure: failure as NSError)) {
+      $0.isRunning = true
+      $0.isStopping = false
+      $0.alert = .error(failure)
+    }
+  }
+
+  func testAlertDismissed() {
+    let store = TestStore(
+      initialState: BackupState(
+        alert: .error(NSError(domain: "test", code: 0))
+      ),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.alertDismissed) {
+      $0.alert = nil
+    }
+  }
+
+  func testExportBackup() {
+    let backupData = "backup-data".data(using: .utf8)!
+
+    let store = TestStore(
+      initialState: BackupState(
+        backup: .init(
+          date: Date(),
+          data: backupData
+        )
+      ),
+      reducer: backupReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.exportTapped) {
+      $0.isExporting = true
+      $0.exportData = backupData
+    }
+
+    store.send(.didExport(failure: nil)) {
+      $0.isExporting = false
+      $0.exportData = nil
+    }
+
+    store.send(.exportTapped) {
+      $0.isExporting = true
+      $0.exportData = backupData
+    }
+
+    let failure = NSError(domain: "test", code: 0)
+    store.send(.didExport(failure: failure)) {
+      $0.isExporting = false
+      $0.exportData = nil
+      $0.alert = .error(failure)
+    }
+  }
+}
+
+private enum Action: Equatable {
+  case didRegisterObserver
+  case didStartBackup(passphrase: String, params: BackupParams)
+  case didResumeBackup
+  case didStopBackup
+  case didRemoveBackup
+  case didFetchContacts(XXModels.Contact.Query)
+}
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index b172793c5095ca4b8e68d1ab5c5e9cbe1c81fc1a..3a57414bb46df0058870adb5078aae2dfc41bf7c 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -1,4 +1,5 @@
 import AppCore
+import BackupFeature
 import ComposableArchitecture
 import ContactsFeature
 import CustomDump
@@ -55,6 +56,7 @@ final class HomeFeatureTests: XCTestCase {
     var messengerDidConnect = 0
     var messengerDidListenForMessages = 0
     var messengerDidLogIn = 0
+    var messengerDidResumeBackup = 0
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
@@ -66,6 +68,8 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { true }
     store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
+    store.environment.messenger.isBackupRunning.run = { false }
+    store.environment.messenger.resumeBackup.run = { messengerDidResumeBackup += 1 }
     store.environment.messenger.cMix.get = {
       var cMix: CMix = .unimplemented
       cMix.addHealthCallback.run = { _ in Cancellable {} }
@@ -82,6 +86,7 @@ final class HomeFeatureTests: XCTestCase {
     XCTAssertNoDifference(messengerDidConnect, 1)
     XCTAssertNoDifference(messengerDidListenForMessages, 1)
     XCTAssertNoDifference(messengerDidLogIn, 1)
+    XCTAssertNoDifference(messengerDidResumeBackup, 1)
 
     store.receive(.networkMonitor(.stop))
     store.receive(.messenger(.didStartRegistered))
@@ -110,6 +115,7 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { true }
     store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
+    store.environment.messenger.isBackupRunning.run = { true }
     store.environment.messenger.cMix.get = {
       var cMix: CMix = .unimplemented
       cMix.addHealthCallback.run = { _ in Cancellable {} }
@@ -504,4 +510,30 @@ final class HomeFeatureTests: XCTestCase {
       $0.contacts = nil
     }
   }
+
+  func testBackupButtonTapped() {
+    let store = TestStore(
+      initialState: HomeState(),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.backupButtonTapped) {
+      $0.backup = BackupState()
+    }
+  }
+
+  func testDidDismissBackup() {
+    let store = TestStore(
+      initialState: HomeState(
+        backup: BackupState()
+      ),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.didDismissBackup) {
+      $0.backup = nil
+    }
+  }
 }
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift
index ed9d193d3592a10dd19a5a5d68d81576cb422f71..bee7f47245c6ebaabd1f615b63881abd6437fddc 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift
@@ -16,7 +16,7 @@ extension MessengerCreate {
       let password = env.generateSecret()
       try env.passwordStorage.save(password)
       let storageDir = env.storageDir
-      try env.fileManager.removeDirectory(storageDir)
+      try env.fileManager.removeItem(storageDir)
       try env.fileManager.createDirectory(storageDir)
       try env.newCMix(
         ndfJSON: String(data: ndfData, encoding: .utf8)!,
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift
index 7d6932badafbae5128694b414df70a32c405fd02..89c472ce7c10a142cd49b559ff149af11997d09b 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift
@@ -23,7 +23,7 @@ extension MessengerDestroy {
       env.e2e.set(nil)
       env.cMix.set(nil)
       env.isListeningForMessages.set(false)
-      try env.fileManager.removeDirectory(env.storageDir)
+      try env.fileManager.removeItem(env.storageDir)
       try env.passwordStorage.remove()
     }
   }
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift
index 08453e23cb8eddf2190ac49b5870489980b88765..4300ec19524ecb06596cdc836fa44f5257460140 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift
@@ -19,6 +19,6 @@ extension MessengerIsBackupRunning {
 
 extension MessengerIsBackupRunning {
   public static let unimplemented = MessengerIsBackupRunning(
-    run: XCTUnimplemented("\(Self.self)")
+    run: XCTUnimplemented("\(Self.self)", placeholder: false)
   )
 }
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift
index b196faca4b578bf3ea9bc534073be015b24dc816..e1f0ec10df6df4a601153cc4ecc6e8c80cb0419c 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift
@@ -33,7 +33,7 @@ extension MessengerRestoreBackup {
       let ndfData = try env.downloadNDF(env.ndfEnvironment)
       let password = env.generateSecret()
       try env.passwordStorage.save(password)
-      try env.fileManager.removeDirectory(storageDir)
+      try env.fileManager.removeItem(storageDir)
       try env.fileManager.createDirectory(storageDir)
       let report = try env.newCMixFromBackup(
         ndfJSON: String(data: ndfData, encoding: .utf8)!,
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift
index 254bc6b9268f03c9c01d6ed8086d2c1e55c1fd89..1512d36100b25a17b2b75175520edf94940ef5a2 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift
@@ -1,3 +1,4 @@
+import Foundation
 import XCTestDynamicOverlay
 import XXClient
 
@@ -33,24 +34,26 @@ extension MessengerStartBackup {
       let paramsData = try params.encode()
       let paramsString = String(data: paramsData, encoding: .utf8)!
       var didAddParams = false
-      func addParams() {
-        guard let backup = env.backup() else { return }
-        backup.addJSON(paramsString)
-        didAddParams = true
-      }
+      var semaphore: DispatchSemaphore? = .init(value: 0)
       let backup = try env.initializeBackup(
         e2eId: e2e.getId(),
         udId: ud.getId(),
         password: password,
         callback: .init { data in
+          semaphore?.wait()
           if !didAddParams {
-            addParams()
+            if let backup = env.backup() {
+              backup.addJSON(paramsString)
+              didAddParams = true
+            }
           } else {
             env.backupCallbacks.registered().handle(data)
           }
         }
       )
       env.backup.set(backup)
+      semaphore?.signal()
+      semaphore = nil
     }
   }
 }
diff --git a/Sources/XXMessengerClient/Utils/BackupStorage.swift b/Sources/XXMessengerClient/Utils/BackupStorage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e85e2e869b6610440b9531f70fd1a7d3fd42bd45
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/BackupStorage.swift
@@ -0,0 +1,78 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct BackupStorage {
+  public struct Backup: Equatable {
+    public init(
+      date: Date,
+      data: Data
+    ) {
+      self.date = date
+      self.data = data
+    }
+
+    public var date: Date
+    public var data: Data
+  }
+
+  public typealias Observer = (Backup?) -> Void
+
+  public var store: (Data) throws -> Void
+  public var observe: (@escaping Observer) -> Cancellable
+  public var remove: () throws -> Void
+}
+
+extension BackupStorage {
+  public static func onDisk(
+    now: @escaping () -> Date = Date.init,
+    fileManager: MessengerFileManager = .live(),
+    path: String = FileManager.default
+      .urls(for: .applicationSupportDirectory, in: .userDomainMask)
+      .first!
+      .appendingPathComponent("backup.xxm")
+      .path
+  ) -> BackupStorage {
+    var observers: [UUID: Observer] = [:]
+    var backup: Backup?
+    func notifyObservers() {
+      observers.values.forEach { $0(backup) }
+    }
+    if let fileData = try? fileManager.loadFile(path),
+       let fileDate = try? fileManager.modifiedTime(path) {
+      backup = Backup(date: fileDate, data: fileData)
+    }
+    return BackupStorage(
+      store: { data in
+        let newBackup = Backup(
+          date: now(),
+          data: data
+        )
+        backup = newBackup
+        notifyObservers()
+        try fileManager.saveFile(path, newBackup.data)
+      },
+      observe: { observer in
+        let id = UUID()
+        observers[id] = observer
+        defer { observers[id]?(backup) }
+        return Cancellable {
+          observers[id] = nil
+        }
+      },
+      remove: {
+        backup = nil
+        notifyObservers()
+        try fileManager.removeItem(path)
+      }
+    )
+  }
+}
+
+extension BackupStorage {
+  public static let unimplemented = BackupStorage(
+    store: XCTUnimplemented("\(Self.self).store"),
+    observe: XCTUnimplemented("\(Self.self).observe", placeholder: Cancellable {}),
+    remove: XCTUnimplemented("\(Self.self).remove")
+  )
+}
diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
index 700990ea64b97c92175995ded858de7fa7486381..ad116d82a6f4a15df0021aa5f8b6ebdd845b7298 100644
--- a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
+++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
@@ -3,8 +3,11 @@ import XCTestDynamicOverlay
 
 public struct MessengerFileManager {
   public var isDirectoryEmpty: (String) -> Bool
-  public var removeDirectory: (String) throws -> Void
+  public var removeItem: (String) throws -> Void
   public var createDirectory: (String) throws -> Void
+  public var saveFile: (String, Data) throws -> Void
+  public var loadFile: (String) throws -> Data?
+  public var modifiedTime: (String) throws -> Date?
 }
 
 extension MessengerFileManager {
@@ -16,7 +19,7 @@ extension MessengerFileManager {
         let contents = try? fileManager.contentsOfDirectory(atPath: path)
         return contents?.isEmpty ?? true
       },
-      removeDirectory: { path in
+      removeItem: { path in
         if fileManager.fileExists(atPath: path) {
           try fileManager.removeItem(atPath: path)
         }
@@ -26,6 +29,16 @@ extension MessengerFileManager {
           atPath: path,
           withIntermediateDirectories: true
         )
+      },
+      saveFile: { path, data in
+        try data.write(to: URL(fileURLWithPath: path))
+      },
+      loadFile: { path in
+        try Data(contentsOf: URL(fileURLWithPath: path))
+      },
+      modifiedTime: { path in
+        let attributes = try fileManager.attributesOfItem(atPath: path)
+        return attributes[.modificationDate] as? Date
       }
     )
   }
@@ -34,7 +47,10 @@ extension MessengerFileManager {
 extension MessengerFileManager {
   public static let unimplemented = MessengerFileManager(
     isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty", placeholder: false),
-    removeDirectory: XCTUnimplemented("\(Self.self).removeDirectory"),
-    createDirectory: XCTUnimplemented("\(Self.self).createDirectory")
+    removeItem: XCTUnimplemented("\(Self.self).removeItem"),
+    createDirectory: XCTUnimplemented("\(Self.self).createDirectory"),
+    saveFile: XCTUnimplemented("\(Self.self).saveFile"),
+    loadFile: XCTUnimplemented("\(Self.self).loadFile"),
+    modifiedTime: XCTUnimplemented("\(Self.self).modifiedTime")
   )
 }
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift
index 2180dda3db578b74a892b2166e66a9beb20357dd..0c8ce3bab33f2923976b113098da9c124c9cc2bf 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift
@@ -15,7 +15,7 @@ final class MessengerCreateTests: XCTestCase {
     var didDownloadNDF: [NDFEnvironment] = []
     var didGenerateSecret: [Int] = []
     var didSavePassword: [Data] = []
-    var didRemoveDirectory: [String] = []
+    var didRemoveItem: [String] = []
     var didCreateDirectory: [String] = []
     var didNewCMix: [DidNewCMix] = []
 
@@ -37,8 +37,8 @@ final class MessengerCreateTests: XCTestCase {
       didSavePassword.append(password)
     }
     env.storageDir = storageDir
-    env.fileManager.removeDirectory = { path in
-      didRemoveDirectory.append(path)
+    env.fileManager.removeItem = { path in
+      didRemoveItem.append(path)
     }
     env.fileManager.createDirectory = { path in
       didCreateDirectory.append(path)
@@ -58,7 +58,7 @@ final class MessengerCreateTests: XCTestCase {
     XCTAssertNoDifference(didDownloadNDF, [.unimplemented])
     XCTAssertNoDifference(didGenerateSecret, [32])
     XCTAssertNoDifference(didSavePassword, [password])
-    XCTAssertNoDifference(didRemoveDirectory, [storageDir])
+    XCTAssertNoDifference(didRemoveItem, [storageDir])
     XCTAssertNoDifference(didCreateDirectory, [storageDir])
     XCTAssertNoDifference(didNewCMix, [.init(
       ndfJSON: String(data: ndf, encoding: .utf8)!,
@@ -108,7 +108,7 @@ final class MessengerCreateTests: XCTestCase {
     env.generateSecret.run = { _ in "password".data(using: .utf8)! }
     env.passwordStorage.save = { _ in }
     env.storageDir = "storage-dir"
-    env.fileManager.removeDirectory = { _ in throw error }
+    env.fileManager.removeItem = { _ in throw error }
     let create: MessengerCreate = .live(env)
 
     XCTAssertThrowsError(try create()) { err in
@@ -126,7 +126,7 @@ final class MessengerCreateTests: XCTestCase {
     env.generateSecret.run = { _ in "password".data(using: .utf8)! }
     env.passwordStorage.save = { _ in }
     env.storageDir = "storage-dir"
-    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.removeItem = { _ in }
     env.fileManager.createDirectory = { _ in throw error }
     let create: MessengerCreate = .live(env)
 
@@ -145,7 +145,7 @@ final class MessengerCreateTests: XCTestCase {
     env.generateSecret.run = { _ in "password".data(using: .utf8)! }
     env.passwordStorage.save = { _ in }
     env.storageDir = "storage-dir"
-    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.removeItem = { _ in }
     env.fileManager.createDirectory = { _ in }
     env.newCMix.run = { _, _, _, _ in throw error }
     let create: MessengerCreate = .live(env)
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift
index ccf999e77e6771724d17e27b6b43ce6a811fb8ae..99a52e8402ac9c93e71988adaf99610f21b1cb95 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift
@@ -9,7 +9,7 @@ final class MessengerDestroyTests: XCTestCase {
     var hasRunningProcesses: [Bool] = [true, true, false]
     var didStopNetworkFollower = 0
     var didSleep: [TimeInterval] = []
-    var didRemoveDirectory: [String] = []
+    var didRemoveItem: [String] = []
     var didSetUD: [UserDiscovery?] = []
     var didSetE2E: [E2E?] = []
     var didSetCMix: [CMix?] = []
@@ -30,7 +30,7 @@ final class MessengerDestroyTests: XCTestCase {
     env.e2e.set = { didSetE2E.append($0) }
     env.cMix.set = { didSetCMix.append($0) }
     env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) }
-    env.fileManager.removeDirectory = { didRemoveDirectory.append($0) }
+    env.fileManager.removeItem = { didRemoveItem.append($0) }
     env.passwordStorage.remove = { didRemovePassword += 1 }
     let destroy: MessengerDestroy = .live(env)
 
@@ -42,7 +42,7 @@ final class MessengerDestroyTests: XCTestCase {
     XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true])
     XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true])
     XCTAssertNoDifference(didSetIsListeningForMessages, [false])
-    XCTAssertNoDifference(didRemoveDirectory, [storageDir])
+    XCTAssertNoDifference(didRemoveItem, [storageDir])
     XCTAssertNoDifference(didRemovePassword, 1)
   }
 
@@ -78,7 +78,7 @@ final class MessengerDestroyTests: XCTestCase {
     env.e2e.set = { didSetE2E.append($0) }
     env.cMix.set = { didSetCMix.append($0) }
     env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) }
-    env.fileManager.removeDirectory = { _ in throw error }
+    env.fileManager.removeItem = { _ in throw error }
     let destroy: MessengerDestroy = .live(env)
 
     XCTAssertThrowsError(try destroy()) { err in
@@ -94,7 +94,7 @@ final class MessengerDestroyTests: XCTestCase {
     struct Error: Swift.Error, Equatable {}
     let error = Error()
     let storageDir = "test-storage-dir"
-    var didRemoveDirectory: [String] = []
+    var didRemoveItem: [String] = []
     var didSetUD: [UserDiscovery?] = []
     var didSetE2E: [E2E?] = []
     var didSetCMix: [CMix?] = []
@@ -107,7 +107,7 @@ final class MessengerDestroyTests: XCTestCase {
     env.cMix.set = { didSetCMix.append($0) }
     env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) }
     env.storageDir = storageDir
-    env.fileManager.removeDirectory = { didRemoveDirectory.append($0) }
+    env.fileManager.removeItem = { didRemoveItem.append($0) }
     env.passwordStorage.remove = { throw error }
     let destroy: MessengerDestroy = .live(env)
 
@@ -118,6 +118,6 @@ final class MessengerDestroyTests: XCTestCase {
     XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true])
     XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true])
     XCTAssertNoDifference(didSetIsListeningForMessages, [false])
-    XCTAssertNoDifference(didRemoveDirectory, [storageDir])
+    XCTAssertNoDifference(didRemoveItem, [storageDir])
   }
 }
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift
index 672adc37550225bdab6a9ea7953c2b5a0b86b248..7e55ddc330cda16e776b36db8abc796e24b9a3d4 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift
@@ -40,7 +40,7 @@ final class MessengerRestoreBackupTests: XCTestCase {
     env.generateSecret.run = { _ in password }
     env.passwordStorage.save = { caughtActions.append(.didSavePassword(password: $0)) }
     env.passwordStorage.load = { password }
-    env.fileManager.removeDirectory = { caughtActions.append(.didRemoveDirectory(path: $0)) }
+    env.fileManager.removeItem = { caughtActions.append(.didRemoveItem(path: $0)) }
     env.fileManager.createDirectory = { caughtActions.append(.didCreateDirectory(path: $0)) }
     env.newCMixFromBackup.run = {
       ndfJSON, storageDir, backupPassphrase, sessionPassword, backupFileContents in
@@ -114,7 +114,7 @@ final class MessengerRestoreBackupTests: XCTestCase {
       .didSavePassword(
         password: password
       ),
-      .didRemoveDirectory(
+      .didRemoveItem(
         path: env.storageDir
       ),
       .didCreateDirectory(
@@ -185,7 +185,7 @@ private enum CaughtAction: Equatable {
   case didSavePassword(
     password: Data
   )
-  case didRemoveDirectory(
+  case didRemoveItem(
     path: String
   )
   case didCreateDirectory(
diff --git a/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..d373ce6e3a88f1644e94ea735a5805dd039631e7
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift
@@ -0,0 +1,113 @@
+import CustomDump
+import XCTest
+@testable import XXMessengerClient
+
+final class BackupStorageTests: XCTestCase {
+  func testStorage() throws {
+    var actions: [Action]!
+
+    var now: Date = .init(0)
+    let path = "backup-path"
+    let fileData = "file-data".data(using: .utf8)!
+    let fileDate = Date(123)
+    var fileManager = MessengerFileManager.unimplemented
+    fileManager.loadFile = { path in
+      actions.append(.didLoadFile(path))
+      return fileData
+    }
+    fileManager.modifiedTime = { path in
+      actions.append(.didGetModifiedTime(path))
+      return fileDate
+    }
+    fileManager.saveFile = { path, data in
+      actions.append(.didSaveFile(path, data))
+    }
+    fileManager.removeItem = { path in
+      actions.append(.didRemoveItem(path))
+    }
+    actions = []
+    let storage: BackupStorage = .onDisk(
+      now: { now },
+      fileManager: fileManager,
+      path: path
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didLoadFile(path),
+      .didGetModifiedTime(path),
+    ])
+
+    actions = []
+    let observerA = storage.observe { backup in
+      actions.append(.didObserve("A", backup))
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didObserve("A", .init(date: fileDate, data: fileData))
+    ])
+
+    actions = []
+    now = .init(1)
+    let data1 = "data-1".data(using: .utf8)!
+    try storage.store(data1)
+
+    XCTAssertNoDifference(actions, [
+      .didObserve("A", .init(date: .init(1), data: data1)),
+      .didSaveFile(path, data1),
+    ])
+
+    actions = []
+    now = .init(2)
+    let observerB = storage.observe { backup in
+      actions.append(.didObserve("B", backup))
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didObserve("B", .init(date: .init(1), data: data1))
+    ])
+
+    actions = []
+    now = .init(3)
+    observerA.cancel()
+    let data2 = "data-2".data(using: .utf8)!
+    try storage.store(data2)
+
+    XCTAssertNoDifference(actions, [
+      .didObserve("B", .init(date: .init(3), data: data2)),
+      .didSaveFile(path, data2),
+    ])
+
+    actions = []
+    now = .init(4)
+    try storage.remove()
+
+    XCTAssertNoDifference(actions, [
+      .didObserve("B", nil),
+      .didRemoveItem(path),
+    ])
+
+    actions = []
+    now = .init(5)
+    observerB.cancel()
+    let data3 = "data-3".data(using: .utf8)!
+    try storage.store(data3)
+
+    XCTAssertNoDifference(actions, [
+      .didSaveFile(path, data3),
+    ])
+  }
+}
+
+private extension Date {
+  init(_ timeIntervalSince1970: TimeInterval) {
+    self.init(timeIntervalSince1970: timeIntervalSince1970)
+  }
+}
+
+private enum Action: Equatable {
+  case didLoadFile(String)
+  case didGetModifiedTime(String)
+  case didObserve(String, BackupStorage.Backup?)
+  case didSaveFile(String, Data)
+  case didRemoveItem(String)
+}