diff --git a/ElixxirDAppsSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElixxirDAppsSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 1c4301a9b6a5409df6f637d7eb64e425929b7ca2..0ee0d575b6265a1319b61f4e9167c7a059296261 100644
--- a/ElixxirDAppsSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ElixxirDAppsSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -9,6 +9,15 @@
         "version" : "0.5.3"
       }
     },
+    {
+      "identity" : "keychainaccess",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
+      "state" : {
+        "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
+        "version" : "4.2.2"
+      }
+    },
     {
       "identity" : "swift-case-paths",
       "kind" : "remoteSourceControl",
diff --git a/Example/ExampleApp/Package.swift b/Example/ExampleApp/Package.swift
index 409928d46b38efc2d747f0fdb1418c5a74e91761..da060c7500d26453edaf44515cc5decedbf78074 100644
--- a/Example/ExampleApp/Package.swift
+++ b/Example/ExampleApp/Package.swift
@@ -31,6 +31,10 @@ let package = Package(
       url: "https://github.com/darrarski/swift-composable-presentation.git",
       .upToNextMajor(from: "0.5.2")
     ),
+    .package(
+      url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
+      .upToNextMajor(from: "4.2.2")
+    ),
   ],
   targets: [
     .target(
@@ -50,6 +54,10 @@ let package = Package(
           name: "ComposablePresentation",
           package: "swift-composable-presentation"
         ),
+        .product(
+          name: "KeychainAccess",
+          package: "KeychainAccess"
+        ),
       ]
     ),
     .testTarget(
@@ -65,6 +73,10 @@ let package = Package(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
         ),
+        .product(
+          name: "ElixxirDAppsSDK",
+          package: "elixxir-dapps-sdk-swift"
+        ),
       ]
     ),
     .testTarget(
diff --git a/Example/ExampleApp/Sources/AppFeature/App.swift b/Example/ExampleApp/Sources/AppFeature/App.swift
index 0374fa69236b9ad17bcbe76cf552fe9b773625ea..b7446b7e0e0849b549d7b802e255038ec0e0f243 100644
--- a/Example/ExampleApp/Sources/AppFeature/App.swift
+++ b/Example/ExampleApp/Sources/AppFeature/App.swift
@@ -1,4 +1,6 @@
+import Combine
 import ComposableArchitecture
+import ElixxirDAppsSDK
 import LandingFeature
 import SessionFeature
 import SwiftUI
@@ -18,8 +20,24 @@ struct App: SwiftUI.App {
 
 extension AppEnvironment {
   static func live() -> AppEnvironment {
-    AppEnvironment(
-      landing: LandingEnvironment(),
+    let clientSubject = CurrentValueSubject<Client?, Never>(nil)
+    let mainScheduler = DispatchQueue.main.eraseToAnyScheduler()
+    let bgScheduler = DispatchQueue(
+      label: "xx.network.dApps.ExampleApp.bg",
+      qos: .background
+    ).eraseToAnyScheduler()
+
+    return AppEnvironment(
+      hasClient: clientSubject.map { $0 != nil }.eraseToAnyPublisher(),
+      mainScheduler: mainScheduler,
+      landing: LandingEnvironment(
+        clientStorage: .live(
+          passwordStorage: .keychain
+        ),
+        setClient: { clientSubject.send($0) },
+        bgScheduler: bgScheduler,
+        mainScheduler: mainScheduler
+      ),
       session: SessionEnvironment()
     )
   }
diff --git a/Example/ExampleApp/Sources/AppFeature/AppFeature.swift b/Example/ExampleApp/Sources/AppFeature/AppFeature.swift
index 07d6812aab59216d9397acc3404a12e2774f0e30..b99904b08b9a7f800fb7d244e5e274f3f3715c57 100644
--- a/Example/ExampleApp/Sources/AppFeature/AppFeature.swift
+++ b/Example/ExampleApp/Sources/AppFeature/AppFeature.swift
@@ -1,3 +1,4 @@
+import Combine
 import ComposableArchitecture
 import ComposablePresentation
 import LandingFeature
@@ -38,11 +39,14 @@ extension AppState.Scene {
 
 enum AppAction: Equatable {
   case viewDidLoad
+  case clientDidChange(hasClient: Bool)
   case landing(LandingAction)
   case session(SessionAction)
 }
 
 struct AppEnvironment {
+  var hasClient: AnyPublisher<Bool, Never>
+  var mainScheduler: AnySchedulerOf<DispatchQueue>
   var landing: LandingEnvironment
   var session: SessionEnvironment
 }
@@ -51,6 +55,22 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment>
 { state, action, env in
   switch action {
   case .viewDidLoad:
+    struct HasClientEffectId: Hashable {}
+    return env.hasClient
+      .removeDuplicates()
+      .map(AppAction.clientDidChange(hasClient:))
+      .receive(on: env.mainScheduler)
+      .eraseToEffect()
+      .cancellable(id: HasClientEffectId(), cancelInFlight: true)
+
+  case .clientDidChange(let hasClient):
+    if hasClient {
+      let sessionState = state.scene.asSession ?? SessionState()
+      state.scene = .session(sessionState)
+    } else {
+      let landingState = state.scene.asLanding ?? LandingState()
+      state.scene = .landing(landingState)
+    }
     return .none
 
   case .landing(_), .session(_):
@@ -75,6 +95,8 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment>
 #if DEBUG
 extension AppEnvironment {
   static let failing = AppEnvironment(
+    hasClient: Empty().eraseToAnyPublisher(),
+    mainScheduler: .failing,
     landing: .failing,
     session: .failing
   )
diff --git a/Example/ExampleApp/Sources/AppFeature/PasswordStorage+Keychain.swift b/Example/ExampleApp/Sources/AppFeature/PasswordStorage+Keychain.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c28a05b8a5b4cabfd4ef71e494b37a7e02d7ae11
--- /dev/null
+++ b/Example/ExampleApp/Sources/AppFeature/PasswordStorage+Keychain.swift
@@ -0,0 +1,14 @@
+import ElixxirDAppsSDK
+import KeychainAccess
+
+extension PasswordStorage {
+  static let keychain: PasswordStorage = {
+    let keychain = KeychainAccess.Keychain(
+      service: "xx.network.client"
+    )
+    return PasswordStorage(
+      save: { password in keychain[data: "password"] = password},
+      load: { try keychain[data: "password"] ?? { throw PasswordStorageMissingPasswordError() }() }
+    )
+  }()
+}
diff --git a/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift b/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
index b145b2500009159d0df667e3befcf6c7e21fed53..a3b1c3b83b19384da34d95c44dce9151ee373bb7 100644
--- a/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
+++ b/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
@@ -1,27 +1,122 @@
+import Combine
 import ComposableArchitecture
+import ElixxirDAppsSDK
 
 public struct LandingState: Equatable {
-  public init() {}
+  public init(
+    hasStoredClient: Bool = false,
+    isMakingClient: Bool = false,
+    isRemovingClient: Bool = false
+  ) {
+    self.hasStoredClient = hasStoredClient
+    self.isMakingClient = isMakingClient
+    self.isRemovingClient = isRemovingClient
+  }
+
+  var hasStoredClient: Bool
+  var isMakingClient: Bool
+  var isRemovingClient: Bool
 }
 
 public enum LandingAction: Equatable {
   case viewDidLoad
+  case makeClient
+  case didMakeClient
+  case didFailMakingClient(NSError)
+  case removeStoredClient
+  case didRemoveStoredClient
+  case didFailRemovingStoredClient(NSError)
 }
 
 public struct LandingEnvironment {
-  public init() {}
+  public init(
+    clientStorage: ClientStorage,
+    setClient: @escaping (Client) -> Void,
+    bgScheduler: AnySchedulerOf<DispatchQueue>,
+    mainScheduler: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.clientStorage = clientStorage
+    self.setClient = setClient
+    self.bgScheduler = bgScheduler
+    self.mainScheduler = mainScheduler
+  }
+
+  public var clientStorage: ClientStorage
+  public var setClient: (Client) -> Void
+  public var bgScheduler: AnySchedulerOf<DispatchQueue>
+  public var mainScheduler: AnySchedulerOf<DispatchQueue>
 }
 
 public let landingReducer = Reducer<LandingState, LandingAction, LandingEnvironment>
 { state, action, env in
   switch action {
   case .viewDidLoad:
+    state.hasStoredClient = env.clientStorage.hasStoredClient()
+    return .none
+
+  case .makeClient:
+    state.isMakingClient = true
+    return Effect.future { fulfill in
+      do {
+        if env.clientStorage.hasStoredClient() {
+          env.setClient(try env.clientStorage.loadClient())
+        } else {
+          env.setClient(try env.clientStorage.createClient())
+        }
+        fulfill(.success(.didMakeClient))
+      } catch {
+        fulfill(.success(.didFailMakingClient(error as NSError)))
+      }
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .didMakeClient:
+    state.isMakingClient = false
+    state.hasStoredClient = env.clientStorage.hasStoredClient()
+    return .none
+
+  case .didFailMakingClient(let error):
+    state.isMakingClient = false
+    state.hasStoredClient = env.clientStorage.hasStoredClient()
+    // TODO: handle error
+    return .none
+
+  case .removeStoredClient:
+    state.isRemovingClient = true
+    return Effect.future { fulfill in
+      do {
+        try env.clientStorage.removeClient()
+        fulfill(.success(.didRemoveStoredClient))
+      } catch {
+        fulfill(.success(.didFailRemovingStoredClient(error as NSError)))
+      }
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .didRemoveStoredClient:
+    state.isRemovingClient = false
+    state.hasStoredClient = env.clientStorage.hasStoredClient()
+    return .none
+
+  case .didFailRemovingStoredClient(let error):
+    state.isRemovingClient = false
+    state.hasStoredClient = env.clientStorage.hasStoredClient()
+    // TODO: handle error
     return .none
   }
 }
 
 #if DEBUG
 extension LandingEnvironment {
-  public static let failing = LandingEnvironment()
+  public static let failing = LandingEnvironment(
+    clientStorage: .failing,
+    setClient: { _ in fatalError() },
+    bgScheduler: .failing,
+    mainScheduler: .failing
+  )
 }
 #endif
diff --git a/Example/ExampleApp/Sources/LandingFeature/LandingView.swift b/Example/ExampleApp/Sources/LandingFeature/LandingView.swift
index 5ebc5e89f90862a08cf02ed96e9541c108fcb67c..325cbe635645c7ed4d98784f3dfb380a2ebcdb61 100644
--- a/Example/ExampleApp/Sources/LandingFeature/LandingView.swift
+++ b/Example/ExampleApp/Sources/LandingFeature/LandingView.swift
@@ -9,15 +9,56 @@ public struct LandingView: View {
   let store: Store<LandingState, LandingAction>
 
   struct ViewState: Equatable {
-    init(state: LandingState) {}
+    let hasStoredClient: Bool
+    let isMakingClient: Bool
+    let isRemovingClient: Bool
+
+    init(state: LandingState) {
+      hasStoredClient = state.hasStoredClient
+      isMakingClient = state.isMakingClient
+      isRemovingClient = state.isRemovingClient
+    }
+
+    var isLoading: Bool {
+      isMakingClient ||
+      isRemovingClient
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
-      Text("LandingView")
-        .task {
-          viewStore.send(.viewDidLoad)
+      Form {
+        Button {
+          viewStore.send(.makeClient)
+        } label: {
+          HStack {
+            Text(viewStore.hasStoredClient ? "Load stored client" : "Create new client")
+            Spacer()
+            if viewStore.isMakingClient {
+              ProgressView()
+            }
+          }
+        }
+
+        if viewStore.hasStoredClient {
+          Button(role: .destructive) {
+            viewStore.send(.removeStoredClient)
+          } label: {
+            HStack {
+              Text("Remove stored client")
+              Spacer()
+              if viewStore.isRemovingClient {
+                ProgressView()
+              }
+            }
+          }
         }
+      }
+      .navigationTitle("Landing")
+      .disabled(viewStore.isLoading)
+      .task {
+        viewStore.send(.viewDidLoad)
+      }
     }
   }
 }
diff --git a/Example/ExampleApp/Sources/SessionFeature/SessionView.swift b/Example/ExampleApp/Sources/SessionFeature/SessionView.swift
index 781ee440613cdf84130a362ea97c4397c7a13d0f..eb1fcf0071a4e534f867c443b2913cbab1ec0779 100644
--- a/Example/ExampleApp/Sources/SessionFeature/SessionView.swift
+++ b/Example/ExampleApp/Sources/SessionFeature/SessionView.swift
@@ -15,6 +15,7 @@ public struct SessionView: View {
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Text("SessionView")
+        .navigationTitle("Session")
         .task {
           viewStore.send(.viewDidLoad)
         }
diff --git a/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift b/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
index 2fe7038c8df495223184b2fa8303ecf31678a02b..96a0b07759825f391e32584051116f09acf55934 100644
--- a/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -1,15 +1,50 @@
+import Combine
 import ComposableArchitecture
+import LandingFeature
+import SessionFeature
 import XCTest
 @testable import AppFeature
 
 final class AppFeatureTests: XCTestCase {
   func testViewDidLoad() throws {
+    let hasClient = PassthroughSubject<Bool, Never>()
+    let mainScheduler = DispatchQueue.test
+
+    var env = AppEnvironment.failing
+    env.hasClient = hasClient.eraseToAnyPublisher()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
     let store = TestStore(
       initialState: AppState(),
       reducer: appReducer,
-      environment: .failing
+      environment: env
     )
 
     store.send(.viewDidLoad)
+
+    hasClient.send(false)
+    mainScheduler.advance()
+
+    store.receive(.clientDidChange(hasClient: false))
+
+    hasClient.send(true)
+    mainScheduler.advance()
+
+    store.receive(.clientDidChange(hasClient: true)) {
+      $0.scene = .session(SessionState())
+    }
+
+    hasClient.send(true)
+    mainScheduler.advance()
+
+    hasClient.send(false)
+    mainScheduler.advance()
+
+    store.receive(.clientDidChange(hasClient: false)) {
+      $0.scene = .landing(LandingState())
+    }
+
+    hasClient.send(completion: .finished)
+    mainScheduler.advance()
   }
 }
diff --git a/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift b/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
index 389863f66901186aecf835bc3c55da017b155bca..3083d97ab257c31f4d1d2450ec50cd75ff1cc772 100644
--- a/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
+++ b/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
@@ -4,12 +4,182 @@ import XCTest
 
 final class LandingFeatureTests: XCTestCase {
   func testViewDidLoad() throws {
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { true }
+
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: env
+    )
+
+    store.send(.viewDidLoad) {
+      $0.hasStoredClient = true
+    }
+  }
+
+  func testCreateClient() {
+    var hasStoredClient = false
+    var didSetClient = false
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { hasStoredClient }
+    env.clientStorage.createClient = { .failing }
+    env.setClient = { _ in didSetClient = true }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: env
+    )
+
+    store.send(.makeClient) {
+      $0.isMakingClient = true
+    }
+
+    bgScheduler.advance()
+
+    XCTAssertTrue(didSetClient)
+
+    hasStoredClient = true
+    mainScheduler.advance()
+
+    store.receive(.didMakeClient) {
+      $0.isMakingClient = false
+      $0.hasStoredClient = true
+    }
+  }
+
+  func testLoadStoredClient() {
+    var didSetClient = false
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { true }
+    env.clientStorage.loadClient = { .failing }
+    env.setClient = { _ in didSetClient = true }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: env
+    )
+
+    store.send(.makeClient) {
+      $0.isMakingClient = true
+    }
+
+    bgScheduler.advance()
+
+    XCTAssertTrue(didSetClient)
+
+    mainScheduler.advance()
+
+    store.receive(.didMakeClient) {
+      $0.isMakingClient = false
+      $0.hasStoredClient = true
+    }
+  }
+
+  func testMakeClientFailure() {
+    let error = NSError(domain: "test", code: 1234)
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { false }
+    env.clientStorage.createClient = { throw error }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: env
+    )
+
+    store.send(.makeClient) {
+      $0.isMakingClient = true
+    }
+
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didFailMakingClient(error)) {
+      $0.isMakingClient = false
+      $0.hasStoredClient = false
+    }
+  }
+
+  func testRemoveStoredClient() {
+    var hasStoredClient = true
+    var didRemoveClient = false
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { hasStoredClient }
+    env.clientStorage.removeClient = { didRemoveClient = true }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
     let store = TestStore(
       initialState: LandingState(),
       reducer: landingReducer,
-      environment: .failing
+      environment: env
     )
 
-    store.send(.viewDidLoad)
+    store.send(.removeStoredClient) {
+      $0.isRemovingClient = true
+    }
+
+    bgScheduler.advance()
+
+    XCTAssertTrue(didRemoveClient)
+
+    hasStoredClient = false
+    mainScheduler.advance()
+
+    store.receive(.didRemoveStoredClient) {
+      $0.isRemovingClient = false
+      $0.hasStoredClient = false
+    }
+  }
+
+  func testRemoveStoredClientFailure() {
+    let error = NSError(domain: "test", code: 1234)
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = LandingEnvironment.failing
+    env.clientStorage.hasStoredClient = { true }
+    env.clientStorage.removeClient = { throw error }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: env
+    )
+
+    store.send(.removeStoredClient) {
+      $0.isRemovingClient = true
+    }
+
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didFailRemovingStoredClient(error)) {
+      $0.isRemovingClient = false
+      $0.hasStoredClient = true
+    }
   }
 }