diff --git a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift
new file mode 100644
index 000000000..120149d23
--- /dev/null
+++ b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift	
@@ -0,0 +1,140 @@
+//: [Previous](@previous)
+
+//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting
+//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings`
+//: in the `File Inspector` is `Platform = macOS`. This is because
+//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should
+//: be set to build for `macOS` unless specified.
+
+import PlaygroundSupport
+import Foundation
+import ParseSwift
+PlaygroundPage.current.needsIndefiniteExecution = true
+
+//: In order to enable offline mode you need to set offlinePolicy to either `create` or `save`
+//: `save` will allow you to save and fetch objects.
+//: `create` will allow you to create, save and fetch objects.
+//: Note that `create` will require you to enable customObjectIds.
+ParseSwift.initialize(applicationId: "applicationId",
+                      clientKey: "clientKey",
+                      masterKey: "masterKey",
+                      serverURL: URL(string: "http://localhost:1337/1")!,
+                      offlinePolicy: .create,
+                      requiringCustomObjectIds: true,
+                      usingEqualQueryConstraint: false,
+                      usingDataProtectionKeychain: false)
+
+struct GameScore: ParseObject {
+    var objectId: String?
+    var createdAt: Date?
+    var updatedAt: Date?
+    var ACL: ParseACL?
+    var originalData: Data?
+
+    //: Your own properties.
+    var points: Int?
+    var timeStamp: Date? = Date()
+    var oldScore: Int?
+    var isHighest: Bool?
+
+    /*:
+     Optional - implement your own version of merge
+     for faster decoding after updating your `ParseObject`.
+     */
+    func merge(with object: Self) throws -> Self {
+        var updated = try mergeParse(with: object)
+        if updated.shouldRestoreKey(\.points,
+                                     original: object) {
+            updated.points = object.points
+        }
+        if updated.shouldRestoreKey(\.timeStamp,
+                                     original: object) {
+            updated.timeStamp = object.timeStamp
+        }
+        if updated.shouldRestoreKey(\.oldScore,
+                                     original: object) {
+            updated.oldScore = object.oldScore
+        }
+        if updated.shouldRestoreKey(\.isHighest,
+                                     original: object) {
+            updated.isHighest = object.isHighest
+        }
+        return updated
+    }
+}
+
+var score = GameScore()
+score.points = 200
+score.oldScore = 10
+score.isHighest = true
+do {
+    try score.save()
+} catch {
+    print(error)
+}
+
+//: If you want to use local objects when an internet connection failed,
+//: you need to set useLocalStore()
+let afterDate = Date().addingTimeInterval(-300)
+var query = GameScore.query("points" > 50,
+                            "createdAt" > afterDate)
+    .useLocalStore()
+    .order([.descending("points")])
+
+//: Query asynchronously (preferred way) - Performs work on background
+//: queue and returns to specified callbackQueue.
+//: If no callbackQueue is specified it returns to main queue.
+query.limit(2)
+    .order([.descending("points")])
+    .find(callbackQueue: .main) { results in
+    switch results {
+    case .success(let scores):
+
+        assert(scores.count >= 1)
+        scores.forEach { score in
+            guard let createdAt = score.createdAt else { fatalError() }
+            assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
+            print("Found score: \(score)")
+        }
+
+    case .failure(let error):
+        if error.equalsTo(.objectNotFound) {
+            assertionFailure("Object not found for this query")
+        } else {
+            assertionFailure("Error querying: \(error)")
+        }
+    }
+}
+
+//: Query synchronously (not preferred - all operations on current queue).
+let results = try query.find()
+assert(results.count >= 1)
+results.forEach { score in
+    guard let createdAt = score.createdAt else { fatalError() }
+    assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
+    print("Found score: \(score)")
+}
+
+//: Query first asynchronously (preferred way) - Performs work on background
+//: queue and returns to specified callbackQueue.
+//: If no callbackQueue is specified it returns to main queue.
+query.first { results in
+    switch results {
+    case .success(let score):
+
+        guard score.objectId != nil,
+            let createdAt = score.createdAt else { fatalError() }
+        assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
+        print("Found score: \(score)")
+
+    case .failure(let error):
+        if error.containedIn([.objectNotFound, .invalidQuery]) {
+            assertionFailure("The query is invalid or the object is not found.")
+        } else {
+            assertionFailure("Error querying: \(error)")
+        }
+    }
+}
+
+PlaygroundPage.current.finishExecution()
+//: [Next](@next)
diff --git a/ParseSwift.playground/Pages/16 - Analytics.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/17 - Analytics.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/16 - Analytics.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/17 - Analytics.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/19 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/19 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/20 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/20 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/20 - Cloud Schemas.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/21 - Cloud Schemas.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/20 - Cloud Schemas.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/21 - Cloud Schemas.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/21 - Cloud Push Notifications.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/22 - Cloud Push Notifications.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/21 - Cloud Push Notifications.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/22 - Cloud Push Notifications.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/22 - Cloud Hook Functions.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/23 - Cloud Hook Functions.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/22 - Cloud Hook Functions.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/23 - Cloud Hook Functions.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.playground/Pages/23 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/24 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift
similarity index 100%
rename from ParseSwift.playground/Pages/23 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift
rename to ParseSwift.playground/Pages/24 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift
diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj
index e1ad03430..6c2bee69c 100644
--- a/ParseSwift.xcodeproj/project.pbxproj
+++ b/ParseSwift.xcodeproj/project.pbxproj
@@ -973,6 +973,13 @@
 		91F346C3269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; };
 		91F346C4269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; };
 		91F346C5269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; };
+		CBEF514C295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; };
+		CBEF514D295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; };
+		CBEF514E295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; };
+		CBEF514F295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; };
+		CBEF5152295EF5DA0052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; };
+		CBEF5153295EF5E20052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; };
+		CBEF5154295EF5E30052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; };
 		F971F4F624DE381A006CB79B /* ParseEncoderExtraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */; };
 		F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; };
 		F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; };
@@ -1449,6 +1456,8 @@
 		91F346B8269B766C005727B6 /* CloudViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudViewModel.swift; sourceTree = "<group>"; };
 		91F346BD269B77B5005727B6 /* CloudObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudObservable.swift; sourceTree = "<group>"; };
 		91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudViewModelTests.swift; sourceTree = "<group>"; };
+		CBEF514B295E40CB0052E598 /* LocalStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = "<group>"; };
+		CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLocalStorageTests.swift; sourceTree = "<group>"; };
 		F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderExtraTests.swift; sourceTree = "<group>"; };
 		F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = "<group>"; };
 		F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
@@ -1686,6 +1695,7 @@
 				70385E6328563FD10084D306 /* ParsePushPayloadFirebaseTests.swift */,
 				70212D172855256F00386163 /* ParsePushTests.swift */,
 				917BA4252703DB4600F8D747 /* ParseQueryAsyncTests.swift */,
+				CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */,
 				700AFE02289C3508006C1CD9 /* ParseQueryCacheTests.swift */,
 				7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */,
 				70C7DC1F24D20F180050419B /* ParseQueryTests.swift */,
@@ -2225,6 +2235,7 @@
 		F97B45CB24D9C6F200F4A88B /* Storage */ = {
 			isa = PBXGroup;
 			children = (
+				CBEF514B295E40CB0052E598 /* LocalStorage.swift */,
 				F97B465E24D9C7B500F4A88B /* KeychainStore.swift */,
 				70572670259033A700F0ADD5 /* ParseFileManager.swift */,
 				F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */,
@@ -2815,6 +2826,7 @@
 				F97B462F24D9C74400F4A88B /* BatchUtils.swift in Sources */,
 				70385E802858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */,
 				70CE0AAD28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */,
+				CBEF514C295E40CB0052E598 /* LocalStorage.swift in Sources */,
 				4A82B7F61F254CCE0063D731 /* Parse.swift in Sources */,
 				F97B45EA24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */,
 				F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */,
@@ -2925,6 +2937,7 @@
 				703B092326BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */,
 				70E6B016286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */,
 				70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */,
+				CBEF5152295EF5DA0052E598 /* ParseLocalStorageTests.swift in Sources */,
 				70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */,
 				70F03A562780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */,
 				7C4C0947285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */,
@@ -3129,6 +3142,7 @@
 				4AFDA72A1F26DAE1002AE4FC /* Parse.swift in Sources */,
 				70385E812858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */,
 				70CE0AAE28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */,
+				CBEF514D295E40CB0052E598 /* LocalStorage.swift in Sources */,
 				F97B45EB24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */,
 				F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */,
 				703B093126BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */,
@@ -3248,6 +3262,7 @@
 				703B092526BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */,
 				70E6B018286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */,
 				70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */,
+				CBEF5154295EF5E30052E598 /* ParseLocalStorageTests.swift in Sources */,
 				709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */,
 				70F03A582780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */,
 				7C4C0949285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */,
@@ -3372,6 +3387,7 @@
 				703B092426BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */,
 				70E6B017286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */,
 				70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */,
+				CBEF5153295EF5E20052E598 /* ParseLocalStorageTests.swift in Sources */,
 				70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */,
 				70F03A572780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */,
 				7C4C0948285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */,
@@ -3576,6 +3592,7 @@
 				F97B465924D9C78C00F4A88B /* Remove.swift in Sources */,
 				70385E832858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */,
 				70CE0AB028595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */,
+				CBEF514F295E40CB0052E598 /* LocalStorage.swift in Sources */,
 				70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */,
 				F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */,
 				703B093326BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */,
@@ -3766,6 +3783,7 @@
 				F97B465824D9C78C00F4A88B /* Remove.swift in Sources */,
 				70385E822858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */,
 				70CE0AAF28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */,
+				CBEF514E295E40CB0052E598 /* LocalStorage.swift in Sources */,
 				70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */,
 				F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */,
 				703B093226BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */,
diff --git a/Sources/ParseSwift/Coding/AnyDecodable.swift b/Sources/ParseSwift/Coding/AnyDecodable.swift
index e7bb1f77e..adf872734 100755
--- a/Sources/ParseSwift/Coding/AnyDecodable.swift
+++ b/Sources/ParseSwift/Coding/AnyDecodable.swift
@@ -35,7 +35,7 @@ struct AnyDecodable: Decodable {
     }
 }
 
-protocol _AnyDecodable {
+protocol _AnyDecodable { // swiftlint:disable:this type_name
     var value: Any { get }
     init<T>(_ value: T?)
 }
@@ -74,6 +74,7 @@ extension _AnyDecodable {
 }
 
 extension AnyDecodable: Equatable {
+    // swiftlint:disable:next cyclomatic_complexity
     static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool {
         switch (lhs.value, rhs.value) {
 #if canImport(Foundation)
diff --git a/Sources/ParseSwift/Coding/AnyEncodable.swift b/Sources/ParseSwift/Coding/AnyEncodable.swift
index f7738bee4..876f968f9 100755
--- a/Sources/ParseSwift/Coding/AnyEncodable.swift
+++ b/Sources/ParseSwift/Coding/AnyEncodable.swift
@@ -38,8 +38,7 @@ struct AnyEncodable: Encodable {
 }
 
 @usableFromInline
-protocol _AnyEncodable {
-
+protocol _AnyEncodable { // swiftlint:disable:this type_name
     var value: Any { get }
     init<T>(_ value: T?)
 }
@@ -47,7 +46,6 @@ protocol _AnyEncodable {
 extension AnyEncodable: _AnyEncodable {}
 
 // MARK: - Encodable
-
 extension _AnyEncodable {
     // swiftlint:disable:next cyclomatic_complexity function_body_length
     func encode(to encoder: Encoder) throws {
@@ -110,6 +108,7 @@ extension _AnyEncodable {
     }
 
     #if canImport(Foundation)
+    // swiftlint:disable:next cyclomatic_complexity
     private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
         switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) {
         case "c", "C":
diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift
index 75b92a2a6..db3ef42c6 100644
--- a/Sources/ParseSwift/Coding/ParseEncoder.swift
+++ b/Sources/ParseSwift/Coding/ParseEncoder.swift
@@ -5,8 +5,7 @@
 //  Created by Pranjal Satija on 7/20/20.
 //  Copyright © 2020 Parse. All rights reserved.
 //
-
-//===----------------------------------------------------------------------===//
+// ===----------------------------------------------------------------------===//
 //
 // This source file is part of the Swift.org open source project
 //
@@ -16,10 +15,10 @@
 // See https://swift.org/LICENSE.txt for license information
 // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
 //
-//===----------------------------------------------------------------------===//
-
+// ===----------------------------------------------------------------------===//
 import Foundation
 
+// swiftlint:disable type_name
 /// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary`
 /// containing `Encodable` values (in which case it should be exempt from key conversion strategies).
 ///
diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift
index 60ff25e92..204a578c6 100644
--- a/Sources/ParseSwift/Extensions/URLSession.swift
+++ b/Sources/ParseSwift/Extensions/URLSession.swift
@@ -66,11 +66,17 @@ internal extension URLSession {
                        responseError: Error?,
                        mapper: @escaping (Data) throws -> U) -> Result<U, ParseError> {
         if let responseError = responseError {
-            guard let parseError = responseError as? ParseError else {
-                return .failure(ParseError(code: .unknownError,
-                                           message: "Unable to connect with parse-server: \(responseError)"))
+            if let urlError = responseError as? URLError,
+                urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed {
+                return .failure(ParseError(code: .notConnectedToInternet,
+                                           message: "Unable to connect with the internet: \(responseError)"))
+            } else {
+                guard let parseError = responseError as? ParseError else {
+                    return .failure(ParseError(code: .unknownError,
+                                               message: "Unable to connect with parse-server: \(responseError)"))
+                }
+                return .failure(parseError)
             }
-            return .failure(parseError)
         }
         guard let response = urlResponse else {
             guard let parseError = responseError as? ParseError else {
diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift
index de7f30c4b..c70616285 100644
--- a/Sources/ParseSwift/Objects/ParseObject+async.swift
+++ b/Sources/ParseSwift/Objects/ParseObject+async.swift
@@ -42,9 +42,11 @@ public extension ParseObject {
      - throws: An error of type `ParseError`.
     */
     @discardableResult func save(ignoringCustomObjectIdConfig: Bool = false,
+                                 ignoringLocalStore: Bool = false,
                                  options: API.Options = []) async throws -> Self {
         try await withCheckedThrowingContinuation { continuation in
             self.save(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                      ignoringLocalStore: ignoringLocalStore,
                       options: options,
                       completion: continuation.resume)
         }
@@ -56,9 +58,11 @@ public extension ParseObject {
      - returns: Returns the saved `ParseObject`.
      - throws: An error of type `ParseError`.
     */
-    @discardableResult func create(options: API.Options = []) async throws -> Self {
+    @discardableResult func create(ignoringLocalStore: Bool = false,
+                                   options: API.Options = []) async throws -> Self {
         try await withCheckedThrowingContinuation { continuation in
-            self.create(options: options,
+            self.create(ignoringLocalStore: ignoringLocalStore,
+                        options: options,
                         completion: continuation.resume)
         }
     }
@@ -69,9 +73,11 @@ public extension ParseObject {
      - returns: Returns the saved `ParseObject`.
      - throws: An error of type `ParseError`.
     */
-    @discardableResult func replace(options: API.Options = []) async throws -> Self {
+    @discardableResult func replace(ignoringLocalStore: Bool = false,
+                                    options: API.Options = []) async throws -> Self {
         try await withCheckedThrowingContinuation { continuation in
-            self.replace(options: options,
+            self.replace(ignoringLocalStore: ignoringLocalStore,
+                         options: options,
                          completion: continuation.resume)
         }
     }
@@ -81,10 +87,12 @@ public extension ParseObject {
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - returns: Returns the saved `ParseObject`.
      - throws: An error of type `ParseError`.
-    */
-    @discardableResult internal func update(options: API.Options = []) async throws -> Self {
+     */
+    @discardableResult internal func update(ignoringLocalStore: Bool = false,
+                                            options: API.Options = []) async throws -> Self {
         try await withCheckedThrowingContinuation { continuation in
-            self.update(options: options,
+            self.update(ignoringLocalStore: ignoringLocalStore,
+                        options: options,
                         completion: continuation.resume)
         }
     }
@@ -159,11 +167,13 @@ public extension Sequence where Element: ParseObject {
     @discardableResult func saveAll(batchLimit limit: Int? = nil,
                                     transaction: Bool = configuration.isUsingTransactions,
                                     ignoringCustomObjectIdConfig: Bool = false,
+                                    ignoringLocalStore: Bool = false,
                                     options: API.Options = []) async throws -> [(Result<Self.Element, ParseError>)] {
         try await withCheckedThrowingContinuation { continuation in
             self.saveAll(batchLimit: limit,
                          transaction: transaction,
                          ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                         ignoringLocalStore: ignoringLocalStore,
                          options: options,
                          completion: continuation.resume)
         }
@@ -188,10 +198,12 @@ public extension Sequence where Element: ParseObject {
     */
     @discardableResult func createAll(batchLimit limit: Int? = nil,
                                       transaction: Bool = configuration.isUsingTransactions,
+                                      ignoringLocalStore: Bool = false,
                                       options: API.Options = []) async throws -> [(Result<Self.Element, ParseError>)] {
         try await withCheckedThrowingContinuation { continuation in
             self.createAll(batchLimit: limit,
                            transaction: transaction,
+                           ignoringLocalStore: ignoringLocalStore,
                            options: options,
                            completion: continuation.resume)
         }
@@ -216,10 +228,12 @@ public extension Sequence where Element: ParseObject {
     */
     @discardableResult func replaceAll(batchLimit limit: Int? = nil,
                                        transaction: Bool = configuration.isUsingTransactions,
+                                       ignoringLocalStore: Bool = false,
                                        options: API.Options = []) async throws -> [(Result<Self.Element, ParseError>)] {
         try await withCheckedThrowingContinuation { continuation in
             self.replaceAll(batchLimit: limit,
                             transaction: transaction,
+                            ignoringLocalStore: ignoringLocalStore,
                             options: options,
                             completion: continuation.resume)
         }
@@ -244,10 +258,12 @@ public extension Sequence where Element: ParseObject {
     */
     internal func updateAll(batchLimit limit: Int? = nil,
                             transaction: Bool = configuration.isUsingTransactions,
+                            ignoringLocalStore: Bool = false,
                             options: API.Options = []) async throws -> [(Result<Self.Element, ParseError>)] {
         try await withCheckedThrowingContinuation { continuation in
             self.updateAll(batchLimit: limit,
                            transaction: transaction,
+                           ignoringLocalStore: ignoringLocalStore,
                            options: options,
                            completion: continuation.resume)
         }
@@ -363,6 +379,7 @@ or disable transactions for this call.
 
     func command(method: Method,
                  ignoringCustomObjectIdConfig: Bool = false,
+                 ignoringLocalStore: Bool = false,
                  options: API.Options,
                  callbackQueue: DispatchQueue) async throws -> Self {
         let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options)
@@ -378,15 +395,23 @@ or disable transactions for this call.
             case .update:
                 command = try self.updateCommand()
             }
-            return try await command
+            let commandResult = try await command
                 .executeAsync(options: options,
                               callbackQueue: callbackQueue,
                               childObjects: savedChildObjects,
                               childFiles: savedChildFiles)
+            if !ignoringLocalStore {
+                try? saveLocally(method: method)
+            }
+            return commandResult
         } catch {
             let defaultError = ParseError(code: .unknownError,
                                           message: error.localizedDescription)
             let parseError = error as? ParseError ?? defaultError
+
+            if !ignoringLocalStore {
+                try? saveLocally(method: method, error: parseError)
+            }
             throw parseError
         }
     }
@@ -398,6 +423,7 @@ internal extension Sequence where Element: ParseObject {
                       batchLimit limit: Int?,
                       transaction: Bool,
                       ignoringCustomObjectIdConfig: Bool = false,
+                      ignoringLocalStore: Bool = false,
                       options: API.Options,
                       callbackQueue: DispatchQueue) async throws -> [(Result<Element, ParseError>)] {
         var options = options
@@ -458,11 +484,19 @@ internal extension Sequence where Element: ParseObject {
                                       childFiles: childFiles)
                 returnBatch.append(contentsOf: saved)
             }
+
+            if !ignoringLocalStore {
+                try? saveLocally(method: method)
+            }
             return returnBatch
         } catch {
             let defaultError = ParseError(code: .unknownError,
                                           message: error.localizedDescription)
             let parseError = error as? ParseError ?? defaultError
+
+            if !ignoringLocalStore {
+                try? saveLocally(method: method, error: parseError)
+            }
             throw parseError
         }
     }
diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift
index 84db2f227..0225f517c 100644
--- a/Sources/ParseSwift/Objects/ParseObject.swift
+++ b/Sources/ParseSwift/Objects/ParseObject.swift
@@ -478,6 +478,8 @@ transactions for this call.
      - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId`
      when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed
      `objectId` environments. Defaults to false.
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
@@ -501,6 +503,7 @@ transactions for this call.
         batchLimit limit: Int? = nil,
         transaction: Bool = configuration.isUsingTransactions,
         ignoringCustomObjectIdConfig: Bool = false,
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<[(Result<Element, ParseError>)], ParseError>) -> Void
@@ -513,6 +516,7 @@ transactions for this call.
                                                      batchLimit: limit,
                                                      transaction: transaction,
                                                      ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                                                     ignoringLocalStore: ignoringLocalStore,
                                                      options: options,
                                                      callbackQueue: callbackQueue)
                 completion(.success(objects))
@@ -530,6 +534,7 @@ transactions for this call.
                      batchLimit: limit,
                      transaction: transaction,
                      ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                     ignoringLocalStore: ignoringLocalStore,
                      options: options,
                      callbackQueue: callbackQueue,
                      completion: completion)
@@ -543,6 +548,8 @@ transactions for this call.
      Defaults to 50.
      - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that
      prevents the transaction from completing, then none of the objects are committed to the Parse Server database.
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
@@ -556,6 +563,7 @@ transactions for this call.
     func createAll( // swiftlint:disable:this function_body_length cyclomatic_complexity
         batchLimit limit: Int? = nil,
         transaction: Bool = configuration.isUsingTransactions,
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<[(Result<Element, ParseError>)], ParseError>) -> Void
@@ -567,6 +575,7 @@ transactions for this call.
                 let objects = try await batchCommand(method: method,
                                                      batchLimit: limit,
                                                      transaction: transaction,
+                                                     ignoringLocalStore: ignoringLocalStore,
                                                      options: options,
                                                      callbackQueue: callbackQueue)
                 completion(.success(objects))
@@ -583,6 +592,7 @@ transactions for this call.
         batchCommand(method: method,
                      batchLimit: limit,
                      transaction: transaction,
+                     ignoringLocalStore: ignoringLocalStore,
                      options: options,
                      callbackQueue: callbackQueue,
                      completion: completion)
@@ -596,6 +606,8 @@ transactions for this call.
      Defaults to 50.
      - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that
      prevents the transaction from completing, then none of the objects are committed to the Parse Server database.
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
@@ -609,6 +621,7 @@ transactions for this call.
     func replaceAll( // swiftlint:disable:this function_body_length cyclomatic_complexity
         batchLimit limit: Int? = nil,
         transaction: Bool = configuration.isUsingTransactions,
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<[(Result<Element, ParseError>)], ParseError>) -> Void
@@ -620,6 +633,7 @@ transactions for this call.
                 let objects = try await batchCommand(method: method,
                                                      batchLimit: limit,
                                                      transaction: transaction,
+                                                     ignoringLocalStore: ignoringLocalStore,
                                                      options: options,
                                                      callbackQueue: callbackQueue)
                 completion(.success(objects))
@@ -636,6 +650,7 @@ transactions for this call.
         batchCommand(method: method,
                      batchLimit: limit,
                      transaction: transaction,
+                     ignoringLocalStore: ignoringLocalStore,
                      options: options,
                      callbackQueue: callbackQueue,
                      completion: completion)
@@ -649,6 +664,8 @@ transactions for this call.
      Defaults to 50.
      - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that
      prevents the transaction from completing, then none of the objects are committed to the Parse Server database.
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
@@ -662,6 +679,7 @@ transactions for this call.
     internal func updateAll( // swiftlint:disable:this function_body_length cyclomatic_complexity
         batchLimit limit: Int? = nil,
         transaction: Bool = configuration.isUsingTransactions,
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<[(Result<Element, ParseError>)], ParseError>) -> Void
@@ -673,6 +691,7 @@ transactions for this call.
                 let objects = try await batchCommand(method: method,
                                                      batchLimit: limit,
                                                      transaction: transaction,
+                                                     ignoringLocalStore: ignoringLocalStore,
                                                      options: options,
                                                      callbackQueue: callbackQueue)
                 completion(.success(objects))
@@ -689,6 +708,7 @@ transactions for this call.
         batchCommand(method: method,
                      batchLimit: limit,
                      transaction: transaction,
+                     ignoringLocalStore: ignoringLocalStore,
                      options: options,
                      callbackQueue: callbackQueue,
                      completion: completion)
@@ -699,6 +719,7 @@ transactions for this call.
                                batchLimit limit: Int?,
                                transaction: Bool,
                                ignoringCustomObjectIdConfig: Bool = false,
+                               ignoringLocalStore: Bool = false,
                                options: API.Options,
                                callbackQueue: DispatchQueue,
                                completion: @escaping (Result<[(Result<Element, ParseError>)], ParseError>) -> Void) {
@@ -800,10 +821,16 @@ transactions for this call.
                         case .success(let saved):
                             returnBatch.append(contentsOf: saved)
                             if completed == (batches.count - 1) {
+                                if !ignoringLocalStore {
+                                    try? saveLocally(method: method)
+                                }
                                 completion(.success(returnBatch))
                             }
                             completed += 1
                         case .failure(let error):
+                            if !ignoringLocalStore {
+                                try? saveLocally(method: method, error: error)
+                            }
                             completion(.failure(error))
                             return
                         }
@@ -1153,7 +1180,9 @@ extension ParseObject {
     */
     @discardableResult
     public func save(ignoringCustomObjectIdConfig: Bool = false,
+                     ignoringLocalStore: Bool = false,
                      options: API.Options = []) throws -> Self {
+        let method = Method.save
         var childObjects: [String: PointerType]?
         var childFiles: [UUID: ParseFile]?
         var error: ParseError?
@@ -1170,13 +1199,27 @@ extension ParseObject {
         group.wait()
 
         if let error = error {
+            if !ignoringLocalStore {
+                try? saveLocally(method: method, error: error)
+            }
             throw error
         }
 
-        return try saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
-            .execute(options: options,
-                     childObjects: childObjects,
-                     childFiles: childFiles)
+        do {
+            let commandResult = try saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
+                .execute(options: options,
+                         childObjects: childObjects,
+                         childFiles: childFiles)
+            if !ignoringLocalStore {
+                try? saveLocally(method: method)
+            }
+            return commandResult
+        } catch {
+            if !ignoringLocalStore {
+                try? saveLocally(method: method, error: error)
+            }
+            throw error
+        }
     }
 
     /**
@@ -1185,6 +1228,8 @@ extension ParseObject {
      - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId`
      when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed
      `objectId` environments. Defaults to false.
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
@@ -1201,6 +1246,7 @@ extension ParseObject {
     */
     public func save(
         ignoringCustomObjectIdConfig: Bool = false,
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<Self, ParseError>) -> Void
@@ -1211,8 +1257,10 @@ extension ParseObject {
             do {
                 let object = try await command(method: method,
                                                ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                                               ignoringLocalStore: ignoringLocalStore,
                                                options: options,
                                                callbackQueue: callbackQueue)
+
                 completion(.success(object))
             } catch {
                 let defaultError = ParseError(code: .unknownError,
@@ -1226,6 +1274,7 @@ extension ParseObject {
         #else
         command(method: method,
                 ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig,
+                ignoringLocalStore: ignoringLocalStore,
                 options: options,
                 callbackQueue: callbackQueue,
                 completion: completion)
@@ -1234,13 +1283,16 @@ extension ParseObject {
 
     /**
      Creates the `ParseObject` *asynchronously* and executes the given callback block.
-
+     
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
      It should have the following argument signature: `(Result<Self, ParseError>)`.
     */
     public func create(
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<Self, ParseError>) -> Void
@@ -1250,6 +1302,7 @@ extension ParseObject {
         Task {
             do {
                 let object = try await command(method: method,
+                                               ignoringLocalStore: ignoringLocalStore,
                                                options: options,
                                                callbackQueue: callbackQueue)
                 completion(.success(object))
@@ -1264,6 +1317,7 @@ extension ParseObject {
         }
         #else
         command(method: method,
+                ignoringLocalStore: ignoringLocalStore,
                 options: options,
                 callbackQueue: callbackQueue,
                 completion: completion)
@@ -1272,13 +1326,16 @@ extension ParseObject {
 
     /**
      Replaces the `ParseObject` *asynchronously* and executes the given callback block.
-
+     
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
      It should have the following argument signature: `(Result<Self, ParseError>)`.
     */
     public func replace(
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<Self, ParseError>) -> Void
@@ -1288,6 +1345,7 @@ extension ParseObject {
         Task {
             do {
                 let object = try await command(method: method,
+                                               ignoringLocalStore: ignoringLocalStore,
                                                options: options,
                                                callbackQueue: callbackQueue)
                 completion(.success(object))
@@ -1302,6 +1360,7 @@ extension ParseObject {
         }
         #else
         command(method: method,
+                ignoringLocalStore: ignoringLocalStore,
                 options: options,
                 callbackQueue: callbackQueue,
                 completion: completion)
@@ -1310,13 +1369,16 @@ extension ParseObject {
 
     /**
      Updates the `ParseObject` *asynchronously* and executes the given callback block.
-
+     
+     - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use
+     since default use should be set via policy on initialize. Defaults to false.
      - parameter options: A set of header options sent to the server. Defaults to an empty set.
      - parameter callbackQueue: The queue to return to after completion. Default value of .main.
      - parameter completion: The block to execute.
      It should have the following argument signature: `(Result<Self, ParseError>)`.
     */
     func update(
+        ignoringLocalStore: Bool = false,
         options: API.Options = [],
         callbackQueue: DispatchQueue = .main,
         completion: @escaping (Result<Self, ParseError>) -> Void
@@ -1326,6 +1388,7 @@ extension ParseObject {
         Task {
             do {
                 let object = try await command(method: method,
+                                               ignoringLocalStore: ignoringLocalStore,
                                                options: options,
                                                callbackQueue: callbackQueue)
                 completion(.success(object))
@@ -1340,6 +1403,7 @@ extension ParseObject {
         }
         #else
         command(method: method,
+                ignoringLocalStore: ignoringLocalStore,
                 options: options,
                 callbackQueue: callbackQueue,
                 completion: completion)
@@ -1348,6 +1412,7 @@ extension ParseObject {
 
     func command(method: Method,
                  ignoringCustomObjectIdConfig: Bool = false,
+                 ignoringLocalStore: Bool = false,
                  options: API.Options,
                  callbackQueue: DispatchQueue,
                  completion: @escaping (Result<Self, ParseError>) -> Void) {
@@ -1371,16 +1436,28 @@ extension ParseObject {
                                       childObjects: savedChildObjects,
                                       childFiles: savedChildFiles,
                                       completion: completion)
+
+                    if !ignoringLocalStore {
+                        try? saveLocally(method: method)
+                    }
                 } catch {
                     let defaultError = ParseError(code: .unknownError,
                                                   message: error.localizedDescription)
                     let parseError = error as? ParseError ?? defaultError
+
+                    if !ignoringLocalStore {
+                        try? saveLocally(method: method, error: parseError)
+                    }
                     callbackQueue.async {
                         completion(.failure(parseError))
                     }
                 }
                 return
             }
+
+            if !ignoringLocalStore {
+                try? saveLocally(method: method, error: parseError)
+            }
             callbackQueue.async {
                 completion(.failure(parseError))
             }
diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift
index cdf961312..555361798 100644
--- a/Sources/ParseSwift/Parse.swift
+++ b/Sources/ParseSwift/Parse.swift
@@ -17,6 +17,7 @@ internal func initialize(applicationId: String,
                          masterKey: String? = nil,
                          serverURL: URL,
                          liveQueryServerURL: URL? = nil,
+                         offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled,
                          requiringCustomObjectIds: Bool = false,
                          usingTransactions: Bool = false,
                          usingEqualQueryConstraint: Bool = false,
@@ -39,6 +40,7 @@ internal func initialize(applicationId: String,
                                            masterKey: masterKey,
                                            serverURL: serverURL,
                                            liveQueryServerURL: liveQueryServerURL,
+                                           offlinePolicy: offlinePolicy,
                                            requiringCustomObjectIds: requiringCustomObjectIds,
                                            usingTransactions: usingTransactions,
                                            usingEqualQueryConstraint: usingEqualQueryConstraint,
@@ -226,6 +228,7 @@ public func initialize(
     masterKey: String? = nil,
     serverURL: URL,
     liveQueryServerURL: URL? = nil,
+    offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled,
     requiringCustomObjectIds: Bool = false,
     usingTransactions: Bool = false,
     usingEqualQueryConstraint: Bool = false,
@@ -248,6 +251,7 @@ public func initialize(
                                            masterKey: masterKey,
                                            serverURL: serverURL,
                                            liveQueryServerURL: liveQueryServerURL,
+                                           offlinePolicy: offlinePolicy,
                                            requiringCustomObjectIds: requiringCustomObjectIds,
                                            usingTransactions: usingTransactions,
                                            usingEqualQueryConstraint: usingEqualQueryConstraint,
diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift
index 8f54a5856..15b56842c 100644
--- a/Sources/ParseSwift/ParseConstants.swift
+++ b/Sources/ParseSwift/ParseConstants.swift
@@ -15,6 +15,9 @@ enum ParseConstants {
     static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
     static let fileManagementLibraryDirectory = "Library/"
     static let fileDownloadsDirectory = "Downloads"
+    static let fileObjectsDirectory = "Objects"
+    static let fetchObjectsFile = "FetchObjects"
+    static let queryObjectsFile = "QueryObjects"
     static let bundlePrefix = "com.parse.ParseSwift"
     static let batchLimit = 50
     static let includeAllKey = "*"
@@ -35,7 +38,7 @@ enum ParseConstants {
     #endif
 }
 
-enum Method: String {
+enum Method: String, Codable {
     case save, create, replace, update
 }
 
diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift
new file mode 100644
index 000000000..827c6e7f6
--- /dev/null
+++ b/Sources/ParseSwift/Storage/LocalStorage.swift
@@ -0,0 +1,522 @@
+//
+//  LocalStorage.swift
+//  
+//
+//  Created by Damian Van de Kauter on 03/12/2022.
+//
+
+import Foundation
+
+public extension ParseObject {
+
+    /**
+     Fetch all local objects.
+     
+     - returns: If objects are more recent on the database, it will replace the local objects and return them.
+     
+     - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects
+     after creating offline objects.
+     */
+    @discardableResult static func fetchLocalStore<T: ParseObject>(_ type: T.Type) async throws -> [T]? {
+        return try await LocalStorage.fetchLocalObjects(type)
+    }
+}
+
+internal var MockLocalStorage: [any ParseObject]?
+
+internal struct LocalStorage {
+    static let fileManager = FileManager.default
+
+    static func save<T: ParseObject>(_ object: T,
+                                     queryIdentifier: String?) throws {
+        let objectData = try ParseCoding.jsonEncoder().encode(object)
+
+        guard let objectId = object.objectId else {
+            throw ParseError(code: .missingObjectId, message: "Object has no valid objectId")
+        }
+
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className)
+        let objectPath = objectsDirectoryPath.appendingPathComponent(objectId)
+
+        if fileManager.fileExists(atPath: objectPath.path) {
+            try objectData.write(to: objectPath)
+        } else {
+            fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil)
+        }
+
+        if let queryIdentifier = queryIdentifier {
+            try self.saveQueryObjects([object], queryIdentifier: queryIdentifier)
+        }
+    }
+
+    static func saveAll<T: ParseObject>(_ objects: [T],
+                                        queryIdentifier: String?) throws {
+        var successObjects: [T] = []
+        for object in objects {
+            let objectData = try ParseCoding.jsonEncoder().encode(object)
+            guard let objectId = object.objectId else {
+                throw ParseError(code: .missingObjectId, message: "Object has no valid objectId")
+            }
+
+            let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className)
+            let objectPath = objectsDirectoryPath.appendingPathComponent(objectId)
+
+            if fileManager.fileExists(atPath: objectPath.path) {
+                try objectData.write(to: objectPath)
+            } else {
+                fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil)
+            }
+
+            successObjects.append(object)
+        }
+
+        if let queryIdentifier = queryIdentifier {
+            try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier)
+        }
+    }
+
+    static func get<U: Decodable>(_ type: U.Type,
+                                  queryIdentifier: String) throws -> U? {
+        guard let queryObjects = try getQueryObjects()[queryIdentifier],
+                let queryObject = queryObjects.first else { return nil }
+
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className)
+        let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId)
+
+        let objectData = try Data(contentsOf: objectPath)
+
+        return try ParseCoding.jsonDecoder().decode(U.self, from: objectData)
+    }
+
+    static func getAll<U: Decodable>(_ type: U.Type,
+                                     queryIdentifier: String) throws -> [U]? {
+        guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil }
+
+        var allObjects: [U] = []
+        for queryObject in queryObjects {
+            let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className)
+            let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId)
+
+            let objectData = try Data(contentsOf: objectPath)
+            if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) {
+                allObjects.append(object)
+            }
+        }
+
+        return (allObjects.isEmpty ? nil : allObjects)
+    }
+
+    static fileprivate func saveFetchObjects<T: ParseObject>(_ objects: [T],
+                                                             method: Method) throws {
+        var fetchObjects = try getFetchObjects()
+        fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) }))
+        fetchObjects = fetchObjects.uniqueObjectsById
+
+        try self.writeFetchObjects(fetchObjects)
+    }
+
+    static fileprivate func removeFetchObjects<T: ParseObject>(_ objects: [T]) throws {
+        var fetchObjects = try getFetchObjects()
+        let objectIds = objects.compactMap({ $0.objectId })
+        fetchObjects.removeAll(where: { removableObject in
+            objectIds.contains(where: { currentObjectId in
+                removableObject.objectId == currentObjectId
+            })
+        })
+        fetchObjects = fetchObjects.uniqueObjectsById
+
+        try self.writeFetchObjects(fetchObjects)
+    }
+
+    static fileprivate func getFetchObjects() throws -> [FetchObject] {
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory()
+        let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile)
+
+        if fileManager.fileExists(atPath: fetchObjectsPath.path) {
+            let jsonData = try Data(contentsOf: fetchObjectsPath)
+            do {
+                if MockLocalStorage != nil { return mockedFetchObjects }
+                return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById
+            } catch {
+                try fileManager.removeItem(at: fetchObjectsPath)
+                if MockLocalStorage != nil { return mockedFetchObjects }
+                return []
+            }
+        } else {
+            if MockLocalStorage != nil { return mockedFetchObjects }
+            return []
+        }
+    }
+
+    static private func writeFetchObjects(_ fetchObjects: [FetchObject]) throws {
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory()
+        let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile)
+
+        if fetchObjects.isEmpty {
+            try? fileManager.removeItem(at: fetchObjectsPath)
+        } else {
+            let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects)
+
+            if fileManager.fileExists(atPath: fetchObjectsPath.path) {
+                try jsonData.write(to: fetchObjectsPath)
+            } else {
+                fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil)
+            }
+        }
+    }
+
+    static private var mockedFetchObjects: [FetchObject] {
+        guard let mockLocalStorage = MockLocalStorage else { return [] }
+        return mockLocalStorage.compactMap({ try? FetchObject($0, method: .save) })
+    }
+
+    static fileprivate func saveQueryObjects<T: ParseObject>(_ objects: [T],
+                                                             queryIdentifier: String) throws {
+        var queryObjects = try getQueryObjects()
+        queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) })
+
+        try self.writeQueryObjects(queryObjects)
+    }
+
+    static fileprivate func getQueryObjects() throws -> [String: [QueryObject]] {
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory()
+        let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile)
+
+        if fileManager.fileExists(atPath: queryObjectsPath.path) {
+            let jsonData = try Data(contentsOf: queryObjectsPath)
+            do {
+                return try ParseCoding.jsonDecoder().decode([String: [QueryObject]].self, from: jsonData)
+            } catch {
+                try fileManager.removeItem(at: queryObjectsPath)
+                return [:]
+            }
+        } else {
+            return [:]
+        }
+    }
+
+    static private func writeQueryObjects(_ queryObjects: [String: [QueryObject]]) throws {
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory()
+        let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile)
+
+        if queryObjects.isEmpty {
+            try? fileManager.removeItem(at: queryObjectsPath)
+        } else {
+            let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects)
+
+            if fileManager.fileExists(atPath: queryObjectsPath.path) {
+                try jsonData.write(to: queryObjectsPath)
+            } else {
+                fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil)
+            }
+        }
+    }
+
+    /**
+     Fetch all local objects.
+     
+     - returns: If objects are more recent on the database, it will replace the local objects and return them.
+     */
+    @discardableResult static func fetchLocalObjects<T: ParseObject>(_ type: T.Type) async throws -> [T]? {
+        let fetchObjects = try getFetchObjects()
+        if fetchObjects.isEmpty {
+            return nil
+        }
+
+        var saveObjects = try fetchObjects
+            .filter({ $0.method == .save })
+            .asParseObjects(type)
+        var createObjects = try fetchObjects
+            .filter({ $0.method == .create })
+            .asParseObjects(type)
+        var replaceObjects = try fetchObjects
+            .filter({ $0.method == .replace })
+            .asParseObjects(type)
+        var updateObjects = try fetchObjects
+            .filter({ $0.method == .update })
+            .asParseObjects(type)
+
+        var cloudObjects: [T] = []
+
+        if Parse.configuration.offlinePolicy.enabled {
+            try await self.fetchLocalStore(.save, objects: &saveObjects, cloudObjects: &cloudObjects)
+        }
+
+        if Parse.configuration.offlinePolicy.canCreate {
+            if Parse.configuration.isRequiringCustomObjectIds {
+                try await self.fetchLocalStore(.create, objects: &createObjects, cloudObjects: &cloudObjects)
+            } else {
+                assertionFailure("Enable custom objectIds")
+            }
+        }
+
+        if Parse.configuration.offlinePolicy.enabled {
+            try await self.fetchLocalStore(.replace, objects: &replaceObjects, cloudObjects: &cloudObjects)
+        }
+
+        if Parse.configuration.offlinePolicy.enabled {
+            try await self.fetchLocalStore(.update, objects: &updateObjects, cloudObjects: &cloudObjects)
+        }
+
+        if cloudObjects.isEmpty {
+            return nil
+        } else {
+            try self.saveAll(cloudObjects, queryIdentifier: nil)
+            return cloudObjects
+        }
+    }
+
+    private static func fetchLocalStore<T: ParseObject>(_ method: Method,
+                                                        objects: inout [T],
+                                                        cloudObjects: inout [T]) async throws {
+        let queryObjects = T.query()
+            .where(containedIn(key: "objectId", array: objects.map({ $0.objectId })))
+            .useLocalStore(false)
+        let foundObjects = try? await queryObjects.find()
+
+        for object in objects {
+            if let matchingObject = foundObjects?.first(where: { $0.objectId == object.objectId }) {
+                if let objectUpdatedAt = object.updatedAt {
+                    if let matchingObjectUpdatedAt = matchingObject.updatedAt {
+                        if objectUpdatedAt < matchingObjectUpdatedAt {
+                            objects.removeAll(where: { $0.objectId == matchingObject.objectId })
+                            cloudObjects.append(matchingObject)
+                        }
+                    }
+                } else {
+                    objects.removeAll(where: { $0.objectId == matchingObject.objectId })
+                    cloudObjects.append(matchingObject)
+                }
+            }
+        }
+
+        if MockLocalStorage == nil {
+            switch method {
+            case .save:
+                try await objects.saveAll(ignoringLocalStore: true)
+            case .create:
+                try await objects.createAll(ignoringLocalStore: true)
+            case .replace:
+                try await objects.replaceAll(ignoringLocalStore: true)
+            case .update:
+                _ = try await objects.updateAll(ignoringLocalStore: true)
+            }
+        }
+
+        try self.removeFetchObjects(objects)
+    }
+}
+
+internal struct FetchObject: Codable {
+    let objectId: String
+    let className: String
+    let updatedAt: Date
+    let method: Method
+
+    init<T: ParseObject>(_ object: T, method: Method) throws {
+        guard let objectId = object.objectId else {
+            throw ParseError(code: .missingObjectId, message: "Object has no valid objectId")
+        }
+        self.objectId = objectId
+        self.className = object.className
+        self.updatedAt = object.updatedAt ?? Date()
+        self.method = method
+    }
+}
+
+internal struct QueryObject: Codable {
+    let objectId: String
+    let className: String
+    let queryDate: Date
+
+    init<T: ParseObject>(_ object: T) throws {
+        guard let objectId = object.objectId else {
+            throw ParseError(code: .missingObjectId, message: "Object has no valid objectId")
+        }
+        self.objectId = objectId
+        self.className = object.className
+        self.queryDate = Date()
+    }
+}
+
+internal extension ParseObject {
+
+    func saveLocally(method: Method? = nil,
+                     queryIdentifier: String? = nil,
+                     error: Error? = nil) throws {
+        if let method = method {
+            switch method {
+            case .save:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects([self], method: method)
+                        }
+                    } else {
+                        try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                    }
+                }
+            case .create:
+                if Parse.configuration.offlinePolicy.canCreate {
+                    if Parse.configuration.isRequiringCustomObjectIds {
+                        if let error = error {
+                            if error.hasNoInternetConnection {
+                                try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                                try LocalStorage.saveFetchObjects([self], method: method)
+                            }
+                        } else {
+                            try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                        }
+                    } else {
+                        assertionFailure("Enable custom objectIds")
+                    }
+                }
+            case .replace:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects([self], method: method)
+                        }
+                    } else {
+                        try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                    }
+                }
+            case .update:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects([self], method: method)
+                        }
+                    } else {
+                        try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+                    }
+                }
+            }
+        } else {
+            if Parse.configuration.offlinePolicy.enabled {
+                try LocalStorage.save(self, queryIdentifier: queryIdentifier)
+            }
+        }
+    }
+}
+
+internal extension Sequence where Element: ParseObject {
+
+    func saveLocally(method: Method? = nil,
+                     queryIdentifier: String? = nil,
+                     error: ParseError? = nil) throws {
+        let objects = map { $0 }
+
+        if let method = method {
+            switch method {
+            case .save:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects(objects, method: method)
+                        }
+                    } else {
+                        try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                    }
+                }
+            case .create:
+                if Parse.configuration.offlinePolicy.canCreate {
+                    if Parse.configuration.isRequiringCustomObjectIds {
+                        if let error = error {
+                            if error.hasNoInternetConnection {
+                                try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                                try LocalStorage.saveFetchObjects(objects, method: method)
+                            }
+                        } else {
+                            try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                        }
+                    } else {
+                        assertionFailure("Enable custom objectIds")
+                    }
+                }
+            case .replace:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects(objects, method: method)
+                        }
+                    } else {
+                        try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                    }
+                }
+            case .update:
+                if Parse.configuration.offlinePolicy.enabled {
+                    if let error = error {
+                        if error.hasNoInternetConnection {
+                            try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                            try LocalStorage.saveFetchObjects(objects, method: method)
+                        }
+                    } else {
+                        try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+                    }
+                }
+            }
+        } else {
+            if Parse.configuration.offlinePolicy.enabled {
+                try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier)
+            }
+        }
+    }
+}
+
+fileprivate extension String {
+
+    /**
+     Creates a hidden file
+     */
+    var hiddenFile: Self {
+        return "." + self
+    }
+}
+
+fileprivate extension Sequence where Element == FetchObject {
+
+    /**
+     Returns a unique array of `FetchObject`'s where each element is the most recent version of itself.
+     */
+    var uniqueObjectsById: [Element] {
+        let fetchObjects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt })
+
+        var uniqueObjects: [Element] = []
+        for fetchObject in fetchObjects {
+            uniqueObjects.append(fetchObjects.first(where: { $0.objectId == fetchObject.objectId }) ?? fetchObject)
+        }
+
+        return uniqueObjects.isEmpty ? fetchObjects : uniqueObjects
+    }
+
+    func asParseObjects<T: ParseObject>(_ type: T.Type) throws -> [T] {
+        let fileManager = FileManager.default
+
+        let fetchObjectIds = map { $0 }.filter({ $0.className == T.className }).map({ $0.objectId })
+
+        let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: T.className)
+        let directoryObjectIds = try fileManager.contentsOfDirectory(atPath: objectsDirectoryPath.path)
+
+        var objects: [T] = []
+
+        for directoryObjectId in directoryObjectIds where fetchObjectIds.contains(directoryObjectId) {
+            let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId,
+                                                                          isDirectory: false)
+
+            if fileManager.fileExists(atPath: contentPath.path) {
+                let jsonData = try Data(contentsOf: contentPath)
+                let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData)
+
+                objects.append(object)
+            }
+        }
+
+        return objects
+    }
+}
diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift
index bde16c2ee..fe800abcf 100644
--- a/Sources/ParseSwift/Storage/ParseFileManager.swift
+++ b/Sources/ParseSwift/Storage/ParseFileManager.swift
@@ -228,6 +228,34 @@ public extension ParseFileManager {
                                     isDirectory: true)
     }
 
+    /**
+     The default directory for all `ParseObject`'s.
+     - parameter className: An optional value, that if set returns the objects directory for a specific class
+     - returns: The objects directory.
+     - throws: An error of type `ParseError`.
+     */
+    static func objectsDirectory(className: String? = nil) throws -> URL {
+        guard let fileManager = ParseFileManager(),
+              let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
+            throw ParseError(code: .unknownError, message: "Cannot create ParseFileManager")
+        }
+        let objectsDirectory = defaultDirectoryPath
+            .appendingPathComponent(ParseConstants.fileObjectsDirectory,
+                                    isDirectory: true)
+        try fileManager.createDirectoryIfNeeded(objectsDirectory.path)
+
+        if let className = className {
+            let classDirectory = objectsDirectory
+                .appendingPathComponent(className,
+                                        isDirectory: true)
+            try fileManager.createDirectoryIfNeeded(classDirectory.path)
+
+            return classDirectory
+        } else {
+            return objectsDirectory
+        }
+    }
+
     /**
      Check if a file exists in the Swift SDK download directory.
      - parameter name: The name of the file to check.
diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift
index 1fb34d44d..54798f5fd 100644
--- a/Sources/ParseSwift/Types/ParseACL.swift
+++ b/Sources/ParseSwift/Types/ParseACL.swift
@@ -8,6 +8,8 @@
 
 import Foundation
 
+// swiftlint:disable large_tuple
+
 /**
  `ParseACL` is used to control which users can access or modify a particular `ParseObject`.
  Each `ParseObject` has its own ACL. You can grant read and write permissions separately 
diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift
index 1fdf8f92c..5093fc013 100644
--- a/Sources/ParseSwift/Types/ParseConfiguration.swift
+++ b/Sources/ParseSwift/Types/ParseConfiguration.swift
@@ -40,6 +40,9 @@ public struct ParseConfiguration {
     /// The live query server URL to connect to Parse Server.
     public internal(set) var liveQuerysServerURL: URL?
 
+    /// Determines wheter or not objects need to be saved locally.
+    public internal(set) var offlinePolicy: OfflinePolicy
+
     /// Requires `objectId`'s to be created on the client.
     public internal(set) var isRequiringCustomObjectIds = false
 
@@ -123,6 +126,7 @@ public struct ParseConfiguration {
      specified when using the SDK on a server.
      - parameter serverURL: The server URL to connect to Parse Server.
      - parameter liveQueryServerURL: The live query server URL to connect to Parse Server.
+     - parameter OfflinePolicy: When enabled, objects will be stored locally for offline usage.
      - parameter requiringCustomObjectIds: Requires `objectId`'s to be created on the client
      side for each object. Must be enabled on the server to work.
      - parameter usingTransactions: Use transactions when saving/updating multiple objects.
@@ -166,6 +170,7 @@ public struct ParseConfiguration {
                 webhookKey: String? = nil,
                 serverURL: URL,
                 liveQueryServerURL: URL? = nil,
+                offlinePolicy: OfflinePolicy = .disabled,
                 requiringCustomObjectIds: Bool = false,
                 usingTransactions: Bool = false,
                 usingEqualQueryConstraint: Bool = false,
@@ -187,6 +192,7 @@ public struct ParseConfiguration {
         self.masterKey = masterKey
         self.serverURL = serverURL
         self.liveQuerysServerURL = liveQueryServerURL
+        self.offlinePolicy = offlinePolicy
         self.isRequiringCustomObjectIds = requiringCustomObjectIds
         self.isUsingTransactions = usingTransactions
         self.isUsingEqualQueryConstraint = usingEqualQueryConstraint
@@ -389,4 +395,34 @@ public struct ParseConfiguration {
                   authentication: authentication)
         self.isMigratingFromObjcSDK = migratingFromObjcSDK
     }
+
+    public enum OfflinePolicy {
+
+        /**
+         When using the `create` Policy, you can get, create and save objects when offline.
+         - warning: Using this Policy requires you to enable `allowingCustomObjectIds`.
+         */
+        case create
+
+        /**
+         When using the `save` Policy, you can get and save objects when offline.
+         */
+        case save
+
+        /**
+         When using the `disabled` Policy, offline usage is disabled.
+         */
+        case disabled
+    }
+}
+
+extension ParseConfiguration.OfflinePolicy {
+
+    var canCreate: Bool {
+        return self == .create
+    }
+
+    var enabled: Bool {
+        return self == .create || self == .save
+    }
 }
diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift
index 2667cde98..387b4d7a8 100644
--- a/Sources/ParseSwift/Types/ParseError.swift
+++ b/Sources/ParseSwift/Types/ParseError.swift
@@ -349,6 +349,11 @@ public struct ParseError: ParseTypeable, Swift.Error {
          */
         case xDomainRequest = 602
 
+        /**
+         Error code indicating that the device is not connected to the internet.
+         */
+        case notConnectedToInternet = 1009
+
         /**
          Error code indicating any other custom error sent from the Parse Server.
          */
@@ -558,3 +563,15 @@ public extension Error {
         containedIn(errorCodes)
     }
 }
+
+internal extension Error {
+
+    /**
+     Validates if the given `ParseError` codes contains the error codes for no internet connection.
+     
+     - returns: A boolean indicating whether or not the `Error` is an internet connection error.
+     */
+    var hasNoInternetConnection: Bool {
+        return self.equalsTo(.notConnectedToInternet) || self.equalsTo(.connectionFailed)
+    }
+}
diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift
index acb1e6c79..d69726abd 100644
--- a/Sources/ParseSwift/Types/Query.swift
+++ b/Sources/ParseSwift/Types/Query.swift
@@ -20,6 +20,7 @@ public struct Query<T>: ParseTypeable where T: ParseObject {
     internal var keys: Set<String>?
     internal var include: Set<String>?
     internal var order: [Order]?
+    internal var useLocalStore: Bool = false
     internal var isCount: Bool?
     internal var explain: Bool?
     internal var hint: AnyCodable?
@@ -45,6 +46,44 @@ public struct Query<T>: ParseTypeable where T: ParseObject {
         Self.className
     }
 
+    internal var queryIdentifier: String {
+        var mutableQuery = self
+        mutableQuery.keys = nil
+        mutableQuery.include = nil
+        mutableQuery.excludeKeys = nil
+        mutableQuery.fields = nil
+
+        guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery),
+              let descriptionString = String(data: jsonData, encoding: .utf8) else {
+            return className
+        }
+
+        //Sets need to be sorted to maintain the same queryIdentifier
+        let sortedKeys = ((keys?.count == 0 ? [] : ["keys"]) +
+                          (keys?.sorted(by: { $0 < $1 }) ?? []))
+        let sortedInclude = ((include?.count == 0 ? [] : ["include"]) +
+                             (include?.sorted(by: { $0 < $1 }) ?? []))
+        let sortedExcludeKeys = ((excludeKeys?.count == 0 ? [] : ["excludeKeys"]) +
+                                 (excludeKeys?.sorted(by: { $0 < $1 }) ?? []))
+        let sortedFieldsKeys = ((fields?.count == 0 ? [] : ["fields"]) +
+                                (fields?.sorted(by: { $0 < $1 }) ?? []))
+
+        let sortedSets = (
+            sortedKeys +
+            sortedInclude +
+            sortedExcludeKeys +
+            sortedFieldsKeys
+        ).joined(separator: "")
+
+        return (
+            className +
+            sortedSets +
+            descriptionString
+        ).replacingOccurrences(of: "[^A-Za-z0-9]+",
+                               with: "",
+                               options: [.regularExpression])
+    }
+
     struct AggregateBody<T>: Codable where T: ParseObject {
         let pipeline: [[String: AnyCodable]]?
         let hint: AnyCodable?
@@ -436,6 +475,17 @@ public struct Query<T>: ParseTypeable where T: ParseObject {
         return mutableQuery
     }
 
+    /**
+     Sort the results of the query based on the `Order` enum.
+      - parameter keys: An array of keys to order by.
+      - returns: The mutated instance of query for easy chaining.
+    */
+    public func useLocalStore(_ state: Bool = true) -> Query<T> {
+        var mutableQuery = self
+        mutableQuery.useLocalStore = state
+        return mutableQuery
+    }
+
     /**
      A variadic list of selected fields to receive updates on when the `Query` is used as a
      `ParseLiveQuery`.
@@ -498,7 +548,23 @@ extension Query: Queryable {
         if limit == 0 {
             return [ResultType]()
         }
-        return try findCommand().execute(options: options)
+        if useLocalStore {
+            do {
+                let objects = try findCommand().execute(options: options)
+                try? objects.saveLocally(queryIdentifier: queryIdentifier)
+
+                return objects
+            } catch let parseError {
+                if parseError.hasNoInternetConnection,
+                   let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) {
+                    return localObjects
+                } else {
+                    throw parseError
+                }
+            }
+        } else {
+            return try findCommand().execute(options: options)
+        }
     }
 
     /**
@@ -548,7 +614,24 @@ extension Query: Queryable {
         do {
             try findCommand().executeAsync(options: options,
                                            callbackQueue: callbackQueue) { result in
-                completion(result)
+                if useLocalStore {
+                    switch result {
+                    case .success(let objects):
+                        try? objects.saveLocally(queryIdentifier: queryIdentifier)
+
+                        completion(result)
+                    case .failure(let failure):
+                        if failure.hasNoInternetConnection,
+                           let localObjects = try? LocalStorage.getAll(ResultType.self,
+                                                                       queryIdentifier: queryIdentifier) {
+                            completion(.success(localObjects))
+                        } else {
+                            completion(.failure(failure))
+                        }
+                    }
+                } else {
+                    completion(result)
+                }
             }
         } catch {
             let parseError = ParseError(code: .unknownError,
@@ -669,16 +752,34 @@ extension Query: Queryable {
                         finished = true
                     }
                 } catch {
-                    let defaultError = ParseError(code: .unknownError,
-                                                  message: error.localizedDescription)
-                    let parseError = error as? ParseError ?? defaultError
-                    callbackQueue.async {
-                        completion(.failure(parseError))
+                    if let urlError = error as? URLError,
+                       (urlError.code == URLError.Code.notConnectedToInternet ||
+                        urlError.code == URLError.Code.dataNotAllowed),
+                       let localObjects = try? LocalStorage.getAll(ResultType.self,
+                                                                   queryIdentifier: queryIdentifier) {
+                        completion(.success(localObjects))
+                    } else {
+                        let defaultError = ParseError(code: .unknownError,
+                                                      message: error.localizedDescription)
+                        let parseError = error as? ParseError ?? defaultError
+
+                        if parseError.hasNoInternetConnection,
+                           let localObjects = try? LocalStorage.getAll(ResultType.self,
+                                                                       queryIdentifier: queryIdentifier) {
+                            completion(.success(localObjects))
+                        } else {
+                            callbackQueue.async {
+                                completion(.failure(parseError))
+                            }
+                        }
                     }
                     return
                 }
             }
 
+            if useLocalStore {
+                try? results.saveLocally(queryIdentifier: queryIdentifier)
+            }
             callbackQueue.async {
                 completion(.success(results))
             }
@@ -699,7 +800,23 @@ extension Query: Queryable {
             throw ParseError(code: .objectNotFound,
                              message: "Object not found on the server.")
         }
-        return try firstCommand().execute(options: options)
+        if useLocalStore {
+            do {
+                let objects = try firstCommand().execute(options: options)
+                try? objects.saveLocally(queryIdentifier: queryIdentifier)
+
+                return objects
+            } catch let parseError {
+                if parseError.hasNoInternetConnection,
+                   let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) {
+                    return localObject
+                } else {
+                    throw parseError
+                }
+            }
+        } else {
+            return try firstCommand().execute(options: options)
+        }
     }
 
     /**
@@ -755,7 +872,23 @@ extension Query: Queryable {
         do {
             try firstCommand().executeAsync(options: options,
                                             callbackQueue: callbackQueue) { result in
-                completion(result)
+                if useLocalStore {
+                    switch result {
+                    case .success(let object):
+                        try? object.saveLocally(queryIdentifier: queryIdentifier)
+
+                        completion(result)
+                    case .failure(let failure):
+                        if failure.hasNoInternetConnection,
+                           let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) {
+                            completion(.success(localObject))
+                        } else {
+                            completion(.failure(failure))
+                        }
+                    }
+                } else {
+                    completion(result)
+                }
             }
         } catch {
             let parseError = ParseError(code: .unknownError,
diff --git a/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift b/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift
index 3d4117f44..a26bb4b1d 100644
--- a/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift
+++ b/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift
@@ -11,6 +11,8 @@ import Foundation
 import XCTest
 @testable import ParseSwift
 
+// swiftlint:disable unused_optional_binding function_body_length type_body_length
+
 class ParseKeychainAccessGroupTests: XCTestCase {
 
     struct User: ParseUser {
@@ -261,6 +263,7 @@ class ParseKeychainAccessGroupTests: XCTestCase {
         XCTAssertEqual(acl, otherAcl)
     }
 
+    // swiftlint:disable:next cyclomatic_complexity
     func testRemoveOldObjectsFromKeychain() throws {
         try userLogin()
         Config.current = .init(welcomeMessage: "yolo", winningNumber: 1)
@@ -299,15 +302,15 @@ class ParseKeychainAccessGroupTests: XCTestCase {
         let deleted = KeychainStore.shared.removeOldObjects(accessGroup: ParseSwift.configuration.keychainAccessGroup)
         XCTAssertTrue(deleted)
         if let _: CurrentUserContainer<User> =
-                try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) {
+            try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) {
             XCTFail("Should be nil")
         }
         if let _: CurrentConfigContainer<Config> =
-                try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) {
+            try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) {
             XCTFail("Should be nil")
         }
         if let _: DefaultACL =
-                try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) {
+            try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) {
             XCTFail("Should be nil")
         }
         guard let _: CurrentInstallationContainer<Installation> =
diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift
new file mode 100644
index 000000000..d181667aa
--- /dev/null
+++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift
@@ -0,0 +1,245 @@
+//
+//  ParseLocalStorageTests.swift
+//  ParseSwiftTests
+//
+//  Created by Damian Van de Kauter on 30/12/2022.
+//  Copyright © 2022 Parse Community. All rights reserved.
+//
+
+#if compiler(>=5.5.2) && canImport(_Concurrency)
+import Foundation
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+import XCTest
+@testable import ParseSwift
+
+final class ParseLocalStorageTests: XCTestCase {
+    struct GameScore: ParseObject {
+        //: These are required by ParseObject
+        var objectId: String?
+        var createdAt: Date?
+        var updatedAt: Date?
+        var ACL: ParseACL?
+        var originalData: Data?
+
+        //: Your own properties
+        var points: Int?
+        var player: String?
+        init() { }
+        //custom initializers
+        init (objectId: String?) {
+            self.objectId = objectId
+        }
+        init(points: Int) {
+            self.points = points
+            self.player = "Jen"
+        }
+        init(points: Int, name: String) {
+            self.points = points
+            self.player = name
+        }
+    }
+
+    override func setUpWithError() throws {
+        try super.setUpWithError()
+        guard let url = URL(string: "http://localhost:1337/1") else {
+            XCTFail("Should create valid URL")
+            return
+        }
+        ParseSwift.initialize(applicationId: "applicationId",
+                              clientKey: "clientKey",
+                              masterKey: "masterKey",
+                              serverURL: url,
+                              offlinePolicy: .create,
+                              requiringCustomObjectIds: true,
+                              usingPostForQuery: true,
+                              testing: true)
+
+        var score1 = GameScore(points: 10)
+        score1.points = 11
+        score1.objectId = "yolo1"
+        score1.createdAt = Date()
+        score1.updatedAt = score1.createdAt
+        score1.ACL = nil
+
+        var score2 = GameScore(points: 10)
+        score2.points = 22
+        score2.objectId = "yolo2"
+        score2.createdAt = Date()
+        score2.updatedAt = score2.createdAt
+        score2.ACL = nil
+
+        MockLocalStorage = [score1, score2]
+    }
+
+    override func tearDownWithError() throws {
+        try super.tearDownWithError()
+    }
+
+    @MainActor
+    func testFetchLocalStore() async throws {
+        try await GameScore.fetchLocalStore(GameScore.self)
+    }
+
+    func testSave() throws {
+        var score = GameScore(points: 10)
+        score.points = 11
+        score.objectId = "yolo"
+        score.createdAt = Date()
+        score.updatedAt = score.createdAt
+        score.ACL = nil
+
+        let query = GameScore.query("objectId" == score.objectId)
+            .useLocalStore()
+        XCTAssertNotEqual(query.queryIdentifier, "")
+
+        try LocalStorage.save(score, queryIdentifier: query.queryIdentifier)
+    }
+
+    func testSaveAll() throws {
+        var score1 = GameScore(points: 10)
+        score1.points = 11
+        score1.objectId = "yolo1"
+        score1.createdAt = Date()
+        score1.updatedAt = score1.createdAt
+        score1.ACL = nil
+
+        var score2 = GameScore(points: 10)
+        score2.points = 22
+        score2.objectId = "yolo2"
+        score2.createdAt = Date()
+        score2.updatedAt = score2.createdAt
+        score2.ACL = nil
+
+        let query = GameScore.query(containedIn(key: "objectId", array: [score1, score2].map({ $0.objectId })))
+            .useLocalStore()
+        XCTAssertNotEqual(query.queryIdentifier, "")
+
+        try LocalStorage.saveAll([score1, score2], queryIdentifier: query.queryIdentifier)
+    }
+
+    func testSaveCheckObjectId() throws {
+        var score1 = GameScore(points: 10)
+        score1.points = 11
+        score1.createdAt = Date()
+        score1.updatedAt = score1.createdAt
+        score1.ACL = nil
+
+        var score2 = GameScore(points: 10)
+        score2.points = 22
+        score2.createdAt = Date()
+        score2.updatedAt = score2.createdAt
+        score2.ACL = nil
+
+        let query = GameScore.query(containedIn(key: "objectId", array: [score1, score2].map({ $0.objectId })))
+            .useLocalStore()
+        XCTAssertNotEqual(query.queryIdentifier, "")
+
+        do {
+            try LocalStorage.saveAll([score1, score2], queryIdentifier: query.queryIdentifier)
+        } catch {
+            XCTAssertTrue(error.equalsTo(.missingObjectId))
+        }
+
+        do {
+            try LocalStorage.save(score1, queryIdentifier: query.queryIdentifier)
+        } catch {
+            XCTAssertTrue(error.equalsTo(.missingObjectId))
+        }
+    }
+
+    func testGet() throws {
+        let query = GameScore.query("objectId" == "yolo")
+            .useLocalStore()
+        XCTAssertNotEqual(query.queryIdentifier, "")
+
+        XCTAssertNoThrow(try LocalStorage.get(GameScore.self, queryIdentifier: query.queryIdentifier))
+    }
+
+    func testGetAll() throws {
+        let query = GameScore.query(containedIn(key: "objectId", array: ["yolo1", "yolo2"]))
+            .useLocalStore()
+        XCTAssertNotEqual(query.queryIdentifier, "")
+
+        XCTAssertNoThrow(try LocalStorage.getAll(GameScore.self, queryIdentifier: query.queryIdentifier))
+    }
+
+    func testSaveLocally() throws {
+        var score1 = GameScore(points: 10)
+        score1.points = 11
+        score1.objectId = "yolo1"
+        score1.createdAt = Date()
+        score1.updatedAt = score1.createdAt
+        score1.ACL = nil
+
+        var score2 = GameScore(points: 10)
+        score2.points = 22
+        score2.objectId = "yolo2"
+        score2.createdAt = Date()
+        score2.updatedAt = score2.createdAt
+        score2.ACL = nil
+
+        let query1 = GameScore.query("objectId" == "yolo1")
+            .useLocalStore()
+        let query2 = GameScore.query("objectId" == ["yolo1", "yolo2"])
+            .useLocalStore()
+
+        XCTAssertNoThrow(try score1.saveLocally(method: .save,
+                                                queryIdentifier: query1.queryIdentifier,
+                                                error: ParseError(code: .notConnectedToInternet,
+                                                                  message: "")))
+        XCTAssertNoThrow(try score1.saveLocally(method: .save,
+                                                queryIdentifier: query1.queryIdentifier))
+
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save,
+                                                          queryIdentifier: query2.queryIdentifier,
+                                                          error: ParseError(code: .notConnectedToInternet,
+                                                                            message: "")))
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save,
+                                                          queryIdentifier: query2.queryIdentifier))
+
+        XCTAssertNoThrow(try score1.saveLocally(method: .create,
+                                                queryIdentifier: query1.queryIdentifier,
+                                                error: ParseError(code: .notConnectedToInternet,
+                                                                  message: "")))
+        XCTAssertNoThrow(try score1.saveLocally(method: .create,
+                                                queryIdentifier: query1.queryIdentifier))
+
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create,
+                                                          queryIdentifier: query2.queryIdentifier,
+                                                          error: ParseError(code: .notConnectedToInternet,
+                                                                            message: "")))
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create,
+                                                          queryIdentifier: query2.queryIdentifier))
+
+        XCTAssertNoThrow(try score1.saveLocally(method: .replace,
+                                                queryIdentifier: query1.queryIdentifier,
+                                                error: ParseError(code: .notConnectedToInternet,
+                                                                  message: "")))
+        XCTAssertNoThrow(try score1.saveLocally(method: .replace,
+                                                queryIdentifier: query1.queryIdentifier))
+
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace,
+                                                          queryIdentifier: query2.queryIdentifier,
+                                                          error: ParseError(code: .notConnectedToInternet,
+                                                                            message: "")))
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace,
+                                                          queryIdentifier: query2.queryIdentifier))
+
+        XCTAssertNoThrow(try score1.saveLocally(method: .update,
+                                                queryIdentifier: query1.queryIdentifier,
+                                                error: ParseError(code: .notConnectedToInternet,
+                                                                  message: "")))
+        XCTAssertNoThrow(try score1.saveLocally(method: .update,
+                                                queryIdentifier: query1.queryIdentifier))
+
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .update,
+                                                          queryIdentifier: query2.queryIdentifier,
+                                                          error: ParseError(code: .notConnectedToInternet,
+                                                                            message: "")))
+        XCTAssertNoThrow(try [score1, score2].saveLocally(method: .update,
+                                                          queryIdentifier: query2.queryIdentifier))
+    }
+}
+#endif
diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift
index d0e239a1a..2ecfc7d56 100644
--- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift
@@ -59,6 +59,7 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len
                               clientKey: "clientKey",
                               masterKey: "masterKey",
                               serverURL: url,
+                              offlinePolicy: .save,
                               usingPostForQuery: true,
                               testing: true)
     }
@@ -102,6 +103,37 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len
         XCTAssert(object.hasSameObjectId(as: scoreOnServer))
     }
 
+    @MainActor
+    func testLocalFind() async throws {
+
+        var scoreOnServer = GameScore(points: 10)
+        scoreOnServer.points = 11
+        scoreOnServer.objectId = "yolo"
+        scoreOnServer.createdAt = Date()
+        scoreOnServer.updatedAt = scoreOnServer.createdAt
+        scoreOnServer.ACL = nil
+
+        let results = QueryResponse<GameScore>(results: [scoreOnServer], count: 1)
+        MockURLProtocol.mockRequests { _ in
+            do {
+                let encoded = try ParseCoding.jsonEncoder().encode(results)
+                return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+            } catch {
+                return nil
+            }
+        }
+
+        let query = GameScore.query
+            .useLocalStore()
+
+        let found = try await query.find()
+        guard let object = found.first else {
+            XCTFail("Should have unwrapped")
+            return
+        }
+        XCTAssert(object.hasSameObjectId(as: scoreOnServer))
+    }
+
     @MainActor
     func testWithCount() async throws {
 
@@ -201,6 +233,35 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len
         XCTAssert(object.hasSameObjectId(as: scoreOnServer))
     }
 
+    @MainActor
+    func testLocalFindAll() async throws {
+
+        var scoreOnServer = GameScore(points: 10)
+        scoreOnServer.objectId = "yarr"
+        scoreOnServer.createdAt = Date()
+        scoreOnServer.updatedAt = scoreOnServer.createdAt
+        scoreOnServer.ACL = nil
+
+        let results = AnyResultsResponse(results: [scoreOnServer])
+        MockURLProtocol.mockRequests { _ in
+            do {
+                let encoded = try ParseCoding.jsonEncoder().encode(results)
+                return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+            } catch {
+                return nil
+            }
+        }
+
+        let found = try await GameScore.query
+            .useLocalStore()
+            .findAll()
+        guard let object = found.first else {
+            XCTFail("Should have unwrapped")
+            return
+        }
+        XCTAssert(object.hasSameObjectId(as: scoreOnServer))
+    }
+
     @MainActor
     func testFindExplain() async throws {
 
diff --git a/Tests/ParseSwiftTests/ParseQueryCacheTests.swift b/Tests/ParseSwiftTests/ParseQueryCacheTests.swift
index 8bc24a04b..afebd2ac9 100644
--- a/Tests/ParseSwiftTests/ParseQueryCacheTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryCacheTests.swift
@@ -65,6 +65,7 @@ class ParseQueryCacheTests: XCTestCase { // swiftlint:disable:this type_body_len
                               clientKey: "clientKey",
                               masterKey: "masterKey",
                               serverURL: url,
+                              offlinePolicy: .save,
                               usingEqualQueryConstraint: false,
                               usingPostForQuery: false,
                               testing: true)
diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift
index 9d4d5031c..a8854d12e 100644
--- a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift
@@ -57,6 +57,7 @@ class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_l
                               clientKey: "clientKey",
                               masterKey: "masterKey",
                               serverURL: url,
+                              offlinePolicy: .save,
                               usingPostForQuery: true,
                               testing: true)
     }
diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift
index 26aef6c82..43388b6b6 100644
--- a/Tests/ParseSwiftTests/ParseQueryTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryTests.swift
@@ -63,6 +63,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
                               clientKey: "clientKey",
                               masterKey: "masterKey",
                               serverURL: url,
+                              offlinePolicy: .save,
                               usingEqualQueryConstraint: false,
                               usingPostForQuery: true,
                               testing: true)
@@ -463,6 +464,38 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
 
     }
 
+    func testLocalFind() {
+        var scoreOnServer = GameScore(points: 10)
+        scoreOnServer.objectId = "yarr"
+        scoreOnServer.createdAt = Date()
+        scoreOnServer.updatedAt = scoreOnServer.createdAt
+        scoreOnServer.ACL = nil
+
+        let results = QueryResponse<GameScore>(results: [scoreOnServer], count: 1)
+        MockURLProtocol.mockRequests { _ in
+            do {
+                let encoded = try ParseCoding.jsonEncoder().encode(results)
+                return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+            } catch {
+                return nil
+            }
+        }
+
+        let query = GameScore.query()
+            .useLocalStore()
+        do {
+
+            guard let score = try query.find(options: []).first else {
+                XCTFail("Should unwrap first object found")
+                return
+            }
+            XCTAssert(score.hasSameObjectId(as: scoreOnServer))
+        } catch {
+            XCTFail(error.localizedDescription)
+        }
+
+    }
+
     func testFindLimit() {
         let query = GameScore.query()
             .limit(0)
@@ -633,6 +666,44 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
         wait(for: [expectation], timeout: 20.0)
     }
 
+    func testLocalFindAllAsync() {
+        var scoreOnServer = GameScore(points: 10)
+        scoreOnServer.objectId = "yarr"
+        scoreOnServer.createdAt = Date()
+        scoreOnServer.updatedAt = scoreOnServer.createdAt
+        scoreOnServer.ACL = nil
+
+        let results = AnyResultsResponse(results: [scoreOnServer])
+        MockURLProtocol.mockRequests { _ in
+            do {
+                let encoded = try ParseCoding.jsonEncoder().encode(results)
+                return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+            } catch {
+                return nil
+            }
+        }
+        let query = GameScore.query()
+            .useLocalStore()
+        let expectation = XCTestExpectation(description: "Count object1")
+        query.findAll { result in
+
+            switch result {
+
+            case .success(let found):
+                guard let score = found.first else {
+                    XCTFail("Should unwrap score count")
+                    expectation.fulfill()
+                    return
+                }
+                XCTAssert(score.hasSameObjectId(as: scoreOnServer))
+            case .failure(let error):
+                XCTFail(error.localizedDescription)
+            }
+            expectation.fulfill()
+        }
+        wait(for: [expectation], timeout: 20.0)
+    }
+
     func testFindAllAsyncErrorSkip() {
         var scoreOnServer = GameScore(points: 10)
         scoreOnServer.objectId = "yarr"
diff --git a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift
index 3d2ff27a9..566af6b04 100644
--- a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift
@@ -44,6 +44,7 @@ class ParseQueryViewModelTests: XCTestCase {
                               clientKey: "clientKey",
                               masterKey: "masterKey",
                               serverURL: url,
+                              offlinePolicy: .save,
                               usingPostForQuery: true,
                               testing: true)
     }