diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 8fa55d889db90a858bb04c68ddfdd16f29787b80..4668d629ddf3b295ce57fe54329d28101e94c7ff 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -117,8 +117,10 @@ let package = Package(
     .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
     ),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index b6eab6a63a9c40a46b41311a419ee2b350bbae8e..a81b863afa2015e6a7d699ba5c921e0dc810e457 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -154,7 +154,13 @@ extension AppEnvironment {
             )
           },
           backup: {
-            BackupEnvironment()
+            BackupEnvironment(
+              messenger: messenger,
+              db: dbManager.getDB,
+              backupStorage: backupStorage,
+              mainQueue: mainQueue,
+              bgQueue: bgQueue
+            )
           }
         )
       }
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
index eebbbc0b55fa328d0d30b658726ac3e63cbc7279..128c375b2625193e207a2c3f0ef50b96d00a3ac7 100644
--- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
@@ -1,28 +1,232 @@
+import AppCore
+import Combine
 import ComposableArchitecture
-import XCTestDynamicOverlay
+import Foundation
+import XXClient
+import XXMessengerClient
+import XXModels
 
 public struct BackupState: Equatable {
-  public init() {}
+  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 {
-  case start
+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() {}
+  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()
+  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 .start:
+  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()
+        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
index 874e61374706e977f8cc0067b20d49b55bacad91..13873f8945ea09c62ba12261213da29057dbf075 100644
--- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import SwiftUI
+import UniformTypeIdentifiers
 
 public struct BackupView: View {
   public init(store: Store<BackupState, BackupAction>) {
@@ -9,20 +10,220 @@ public struct BackupView: View {
   let store: Store<BackupState, BackupAction>
 
   struct ViewState: Equatable {
-    init(state: BackupState) {}
+    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 {
+            newBackupSection(viewStore)
+          }
+          if viewStore.isRunning || viewStore.backup != nil {
+            backupSection(viewStore)
+          }
+        }
+        .disabled(viewStore.isLoading)
+        .alert(
+          store.scope(state: \.alert),
+          dismiss: .alertDismissed
+        )
       }
       .navigationTitle("Backup")
       .task {
-        viewStore.send(.start)
+        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
diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
index a95240ca5ce196d4d81e55327e1b86bf3748f9ca..ebb1510bc0c4f8a602db808931fe2c66ee5d3589 100644
--- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
@@ -1,15 +1,452 @@
 import ComposableArchitecture
 import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
 @testable import BackupFeature
 
 final class BackupFeatureTests: XCTestCase {
-  func testStart() {
+  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(.start)
+    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)
 }