diff --git a/mobile/.gitignore b/mobile/.gitignore
index c27f5ca3f1..1b035f8f3d 100644
--- a/mobile/.gitignore
+++ b/mobile/.gitignore
@@ -24,7 +24,6 @@
 
 # Flutter/Dart/Pub related
 **/doc/api/
-**/ios/
 .dart_tool/
 .flutter-plugins
 .flutter-plugins-dependencies
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 34d58a03fc..fb08564767 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -24,6 +24,8 @@ PODS:
   - path_provider_foundation (0.0.1):
     - Flutter
     - FlutterMacOS
+  - path_provider_ios (0.0.1):
+    - Flutter
   - photo_manager (2.0.0):
     - Flutter
     - FlutterMacOS
@@ -55,6 +57,7 @@ DEPENDENCIES:
   - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
+  - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
   - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
@@ -90,6 +93,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
     :path: ".symlinks/plugins/path_provider_foundation/ios"
+  path_provider_ios:
+    :path: ".symlinks/plugins/path_provider_ios/ios"
   photo_manager:
     :path: ".symlinks/plugins/photo_manager/ios"
   share_plus:
@@ -117,6 +122,7 @@ SPEC CHECKSUMS:
   isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
   package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
   path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
+  path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
   photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
   share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index b89713af80..8d4a8b3f85 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -9,6 +9,8 @@
 /* Begin PBXBuildFile section */
 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
+		65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
 		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -34,6 +36,8 @@
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
 		2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = "<group>"; };
+		65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = "<group>"; };
 		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
 		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -80,6 +84,15 @@
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		65DD438629917FAD0047FFA8 /* BackgroundSync */ = {
+			isa = PBXGroup;
+			children = (
+				65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */,
+				65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */,
+			);
+			path = BackgroundSync;
+			sourceTree = "<group>";
+		};
 		9740EEB11CF90186004384FC /* Flutter */ = {
 			isa = PBXGroup;
 			children = (
@@ -113,6 +126,7 @@
 		97C146F01CF9000F007C117D /* Runner */ = {
 			isa = PBXGroup;
 			children = (
+				65DD438629917FAD0047FFA8 /* BackgroundSync */,
 				FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
 				97C146FA1CF9000F007C117D /* Main.storyboard */,
 				97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -275,8 +289,10 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
 				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+				65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -363,7 +379,6 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 86;
-				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -372,6 +387,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
+				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -499,7 +515,6 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 86;
-				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -508,6 +523,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
+				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -527,7 +543,6 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 86;
-				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -536,6 +551,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
+				MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
 				PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index c87d15a335..83d495ca8a 100644
--- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -43,11 +43,13 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      disableMainThreadChecker = "YES"
       launchStyle = "0"
       useCustomWorkingDirectory = "NO"
       ignoresPersistentStateOnLaunch = "NO"
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
+      enableGPUValidationMode = "1"
       allowLocationSimulation = "YES">
       <BuildableProductRunnable
          runnableDebuggingMode = "0">
diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift
index 70693e4a8c..c9331a780b 100644
--- a/mobile/ios/Runner/AppDelegate.swift
+++ b/mobile/ios/Runner/AppDelegate.swift
@@ -1,13 +1,33 @@
 import UIKit
 import Flutter
+import BackgroundTasks
+import path_provider_ios
+import photo_manager
 
 @UIApplicationMain
 @objc class AppDelegate: FlutterAppDelegate {
+    
   override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
-    GeneratedPluginRegistrant.register(with: self)
-    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+
+      GeneratedPluginRegistrant.register(with: self)
+      BackgroundServicePlugin.registerBackgroundProcessing()
+
+      BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
+            
+      BackgroundServicePlugin.setPluginRegistrantCallback { registry in
+          if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
+              FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
+          }
+          
+          if !registry.hasPlugin("org.cocoapods.photo-manager") {
+              PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
+          }
+      }
+      
+      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
+    
 }
diff --git a/mobile/ios/Runner/Assets.xcassets/Contents.json b/mobile/ios/Runner/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift
new file mode 100644
index 0000000000..1dcf39d1e2
--- /dev/null
+++ b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift
@@ -0,0 +1,305 @@
+//
+//  BackgroundServicePlugin.swift
+//  Runner
+//
+//  Created by Marty Fuhry on 2/14/23.
+//
+
+import Flutter
+import BackgroundTasks
+import path_provider_foundation
+
+class BackgroundServicePlugin: NSObject, FlutterPlugin {
+    
+    public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
+    
+    public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) {
+        flutterPluginRegistrantCallback = callback
+    }
+
+    //  Pause the application in XCode, then enter
+    // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"]
+    // or
+    // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"]
+    // Then resume the application see the background code run
+    // Tested on a physical device, not a simulator
+    // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing.
+    // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command
+    
+    // This is the task ID in Info.plist to register as our background task ID
+    public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch"
+    public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing"
+    
+    // Establish communication with the main isolate and set up the channel call
+    // to this BackgroundServicePlugion()
+    public static func register(with registrar: FlutterPluginRegistrar) {
+        let channel = FlutterMethodChannel(
+            name: "immich/foregroundChannel",
+            binaryMessenger: registrar.messenger()
+        )
+
+        let instance = BackgroundServicePlugin()
+        registrar.addMethodCallDelegate(instance, channel: channel)
+        registrar.addApplicationDelegate(instance)
+    }
+    
+    // Registers the Flutter engine with the plugins, used by the other Background Flutter engine
+    public static func register(engine: FlutterEngine) {
+        GeneratedPluginRegistrant.register(with: engine)
+    }
+
+    // Registers the task IDs from the system so that we can process them here in this class
+    public static func registerBackgroundProcessing() {
+ 
+        let processingRegisterd = BGTaskScheduler.shared.register(
+            forTaskWithIdentifier: backgroundProcessingTaskID,
+            using: nil) { task in
+            if task is BGProcessingTask {
+                handleBackgroundProcessing(task: task as! BGProcessingTask)
+            }
+        }
+        
+        let fetchRegisterd = BGTaskScheduler.shared.register(
+            forTaskWithIdentifier: backgroundFetchTaskID,
+            using: nil) { task in
+            if task is BGAppRefreshTask {
+                handleBackgroundFetch(task: task as! BGAppRefreshTask)
+            }
+        }
+    }
+
+    // Handles the channel methods from Flutter
+    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+        switch call.method {
+            case "enable":
+                handleBackgroundEnable(call: call, result: result)
+                break
+            case "configure":
+                handleConfigure(call: call, result: result)
+                break
+            case "disable":
+                handleDisable(call: call, result: result)
+                break
+            case "isEnabled":
+                handleIsEnabled(call: call, result: result)
+                break
+            case "isIgnoringBatteryOptimizations":
+                result(FlutterMethodNotImplemented)
+                break
+            case "lastBackgroundFetchTime":
+                let defaults = UserDefaults.standard
+                let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
+                result(lastRunTime)
+            case "lastBackgroundProcessingTime":
+                let defaults = UserDefaults.standard
+                let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
+                result(lastRunTime)
+            case "numberOfBackgroundProcesses":
+                handleNumberOfProcesses(call: call, result: result)
+            default:
+                result(FlutterMethodNotImplemented)
+                break
+        }
+    }
+    
+    // Called by the flutter code when enabled so that we can turn on the backround services
+    // and save the callback information to communicate on this method channel
+    public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
+        
+        // Needs to parse the arguments from the method call
+        guard let args = call.arguments as? Array<Any> else {
+            print("Cannot parse args as array: \(call.arguments)")
+            result(FlutterMethodNotImplemented)
+            return
+        }
+        
+        // Requires 3 arguments in the array
+        guard args.count == 3 else {
+            print("Requires 3 arguments and received \(args.count)")
+            result(FlutterMethodNotImplemented)
+            return
+        }
+                
+        // Parses the arguments
+        let callbackHandle = args[0] as? Int64
+        let notificationTitle = args[1] as? String
+        let instant = args[2] as? Bool
+        
+        // Write enabled to settings
+        let defaults = UserDefaults.standard
+        
+        // We are now enabled, so store this
+        defaults.set(true, forKey: "background_service_enabled")
+        
+        // The callback handle is an int64 address to communicate with the main isolate's
+        // entry function
+        defaults.set(callbackHandle, forKey: "callback_handle")
+        
+        // This is not used yet and will need to be implemented
+        defaults.set(notificationTitle, forKey: "notification_title")
+        
+        // Schedule the background services if instant
+        if (instant ?? true) {
+            BackgroundServicePlugin.scheduleBackgroundSync()
+            BackgroundServicePlugin.scheduleBackgroundFetch()
+        }
+        result(true)
+    }
+    
+    // Called by the flutter code at launch to see if the background service is enabled or not
+    func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) {
+        let defaults = UserDefaults.standard
+        let enabled = defaults.value(forKey: "background_service_enabled") as? Bool
+        
+        // False by default
+        result(enabled ?? false)
+    }
+    
+    // Called by the Flutter code whenever a change in configuration is set
+    func handleConfigure(call: FlutterMethodCall, result: FlutterResult) {
+        
+        // Needs to be able to parse the arguments or else fail
+        guard let args = call.arguments as? Array<Any> else {
+            print("Cannot parse args as array: \(call.arguments)")
+            result(FlutterError())
+            return
+        }
+        
+        // Needs to have 4 arguments in the call or else fail
+        guard args.count == 4 else {
+            print("Not enough arguments, 4 required: \(args.count) given")
+            result(FlutterError())
+            return
+        }
+        
+        // Parse the arguments from the method call
+        let requireUnmeteredNetwork = args[0] as? Bool
+        let requireCharging = args[1] as? Bool
+        let triggerUpdateDelay = args[2] as? Int
+        let triggerMaxDelay = args[3] as? Int
+        
+        // Store the values from the call in the defaults
+        let defaults = UserDefaults.standard
+        defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network")
+        defaults.set(requireCharging, forKey: "require_charging")
+        defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay")
+        defaults.set(triggerMaxDelay, forKey: "trigger_max_delay")
+
+        // Cancel the background services and reschedule them
+        BGTaskScheduler.shared.cancelAllTaskRequests()
+        BackgroundServicePlugin.scheduleBackgroundSync()
+        BackgroundServicePlugin.scheduleBackgroundFetch()
+        result(true)
+    }
+    
+    // Returns the number of currently scheduled background processes to Flutter, striclty
+    // for debugging
+    func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) {
+        BGTaskScheduler.shared.getPendingTaskRequests { requests in
+            result(requests.count)
+        }
+    }
+    
+    // Disables the service, cancels all the task requests
+    func handleDisable(call: FlutterMethodCall, result: FlutterResult) {
+        let defaults = UserDefaults.standard
+        defaults.set(false, forKey: "background_service_enabled")
+        
+        BGTaskScheduler.shared.cancelAllTaskRequests()
+        result(true)
+    }
+  
+    // Schedules a short-running background sync to sync only a few photos
+    static func scheduleBackgroundFetch() {
+        // We will only schedule this task to run if the user has explicitely allowed us to backup while
+        // not connected to power
+        let defaults = UserDefaults.standard
+        if defaults.value(forKey: "require_charging") as? Bool == true {
+            return
+        }
+                
+        let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
+        
+        // Use 5 minutes from now as earliest begin date
+        backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60)
+        
+        do {
+            try BGTaskScheduler.shared.submit(backgroundFetch)
+        } catch {
+            print("Could not schedule the background task \(error.localizedDescription)")
+        }
+    }
+    
+    // Schedules a long-running background sync for syncing all of the photos
+    static func scheduleBackgroundSync() {
+        let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID)
+        
+        // We need the values for requiring charging
+        let defaults = UserDefaults.standard
+        let requireCharging = defaults.value(forKey: "require_charging") as? Bool
+        
+        // Always require network connectivity, and set the require charging from the above
+        backgroundProcessing.requiresNetworkConnectivity = true
+        backgroundProcessing.requiresExternalPower = requireCharging ?? true
+                
+        // Use 15 minutes from now as earliest begin date
+        backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
+        
+        do {
+            // Submit the task to the scheduler
+            try BGTaskScheduler.shared.submit(backgroundProcessing)
+        } catch {
+            print("Could not schedule the background task \(error.localizedDescription)")
+        }
+    }
+    
+    // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
+    static func handleBackgroundFetch(task: BGAppRefreshTask) {
+        // Log the time of last background processing to now
+        let defaults = UserDefaults.standard
+        defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
+        
+        // Schedule the next sync task so we can run this again later
+        scheduleBackgroundFetch()
+        
+        // The background sync task should only run for 20 seconds at most
+        BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20)
+    }
+    
+    // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
+    static func handleBackgroundProcessing(task: BGProcessingTask) {
+        // Log the time of last background processing to now
+        let defaults = UserDefaults.standard
+        defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
+        
+        // Schedule the next sync task so we run this again later
+        scheduleBackgroundSync()
+        
+        // We won't specify a max time for the background sync service, so this can run for longer
+        BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
+    }
+    
+    // This is a synchronous function which uses a semaphore to run the background sync worker's run
+    // function, which will create a background Isolate and communicate with the Flutter code to back
+    // up the assets. When it completes, we signal the semaphore and complete the execution allowing the
+    // control to pass back to the caller synchronously
+    static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) {
+
+        let semaphore = DispatchSemaphore(value: 0)
+        DispatchQueue.main.async {
+            let backgroundWorker = BackgroundSyncWorker { _ in
+                semaphore.signal()
+            }
+            task.expirationHandler = {
+                backgroundWorker.cancel()
+                task.setTaskCompleted(success: true)
+            }
+            
+            backgroundWorker.run(maxSeconds: maxSeconds)
+            task.setTaskCompleted(success: true)
+        }
+        semaphore.wait()
+    }
+
+
+}
diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
new file mode 100644
index 0000000000..10f1bb24a1
--- /dev/null
+++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
@@ -0,0 +1,188 @@
+//
+//  BackgroundSyncProcessing.swift
+//  Runner
+//
+//  Created by Marty Fuhry on 2/6/23.
+//
+// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift
+
+import Foundation
+import Flutter
+import BackgroundTasks
+
+// The background worker which creates a new Flutter VM, communicates with it
+// to run the backup job, and then finishes execution and calls back to its callback
+// handler
+class BackgroundSyncWorker {
+
+    // The Flutter engine we create for background execution.
+    // This is not the main Flutter engine which shows the UI,
+    // this is a brand new isolate created and managed in this code
+    // here. It does not share memory with the main
+    // Flutter engine which shows the UI.
+    // It needs to be started up, registered, and torn down here
+    let engine: FlutterEngine? = FlutterEngine(
+        name: "BackgroundImmich"
+    )
+    
+    // The background message passing channel
+    var channel: FlutterMethodChannel?
+    
+    var completionHandler: (UIBackgroundFetchResult) -> Void
+    let taskSessionStart = Date()
+    
+    // We need the completion handler to tell the system when we are done running
+    init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
+        
+        // This is the background message passing channel to be used with the background engine
+        // created here in this platform code
+        self.channel = FlutterMethodChannel(
+            name: "immich/backgroundChannel",
+            binaryMessenger: engine!.binaryMessenger
+        )
+        self.completionHandler = completionHandler
+    }
+    
+    // Handles all of the messages from the Flutter VM called into this platform code
+    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+        switch call.method {
+        case "initialized":
+            // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update
+            self.channel?.invokeMethod(
+                "backgroundProcessing",
+                arguments: nil,
+                result: { flutterResult in
+                    
+                    // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or
+                    // if this execution failed
+                    let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
+                    
+                    // Show the task duration
+                    let taskSessionCompleter = Date()
+                    let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart)
+                    print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)")
+                    
+                    // Complete the execution
+                    self.complete(result)
+                })
+            break
+        case "updateNotification":
+            // TODO: implement update notification
+            result(true)
+            break
+        case "showError":
+            // TODO: implement show error
+            result(true)
+            break
+        case "clearErrorNotifications":
+            // TODO: implement clear error notifications
+            result(true)
+            break
+        case "hasContentChanged":
+            // This is only called for Android, but we provide an implementation here
+            // telling Flutter that we don't have any information about whether the gallery
+            // contents have changed or not, so we can just say "no, they've not changed"
+            result(false)
+            break
+        default:
+            result(FlutterError())
+            self.complete(UIBackgroundFetchResult.failed)
+        }
+    }
+    
+    // Runs the background sync by starting up a new isolate and handling the calls
+    // until it completes
+    public func run(maxSeconds: Int?) {
+        // We need the callback handle to start up the Flutter VM from the entry point
+        let defaults = UserDefaults.standard
+        guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else {
+            // Can't find the callback handle, this is fatal
+            complete(UIBackgroundFetchResult.failed)
+            return
+            
+        }
+        
+        // Use the provided callbackHandle to get the callback function
+        guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
+            // We need this callback or else this is fatal
+            complete(UIBackgroundFetchResult.failed)
+            return
+        }
+        
+        // Sanity check for the engine existing
+        if engine == nil {
+            complete(UIBackgroundFetchResult.failed)
+            return
+        }
+               
+        // Run the engine
+        let isRunning = engine!.run(
+            withEntrypoint: callback.callbackName,
+            libraryURI: callback.callbackLibraryPath
+        )
+        
+        // If this engine isn't running, this is fatal
+        if !isRunning {
+            complete(UIBackgroundFetchResult.failed)
+            return
+        }
+        
+        // If we have a timer, we need to start the timer to cancel ourselves
+        // so that we don't run longer than the provided maxSeconds
+        // After maxSeconds has elapsed, we will invoke "systemStop"
+        if maxSeconds != nil {
+            // Schedule a non-repeating timer to run after maxSeconds
+            let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!),
+                                                      repeats: false) { timer in
+                // The callback invalidates the timer and stops execution
+                timer.invalidate()
+                
+                // If the channel is already deallocated, we don't need to do anything
+                if self.channel == nil {
+                    return
+                }
+                
+                // Tell the Flutter VM to stop backing up now
+                self.channel?.invokeMethod(
+                    "systemStop",
+                    arguments: nil,
+                    result: nil)
+                
+                // Complete the execution
+                self.complete(UIBackgroundFetchResult.newData)
+            }
+        }
+        
+        // Set the handle function to the channel message handler
+        self.channel?.setMethodCallHandler(handle)
+        
+        // Register this to get access to the plugins on the platform channel
+        BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!)
+    }
+    
+    // Cancels execution of this task, used by the system's task expiration handler
+    // which is called shortly before execution is about to expire
+    public func cancel() {
+        // If the channel is already deallocated, we don't need to do anything
+        if self.channel == nil {
+            return
+        }
+        
+        // Tell the Flutter VM to stop backing up now
+        self.channel?.invokeMethod(
+            "systemStop",
+            arguments: nil,
+            result: nil)
+        
+        // Complete the execution
+        self.complete(UIBackgroundFetchResult.newData)
+    }
+
+    // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler
+    private func complete(_ fetchResult: UIBackgroundFetchResult) {
+        engine?.destroyContext()
+        channel = nil
+        completionHandler(fetchResult)
+    }
+}
+
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index a2bd73c438..ae11697bd5 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -2,6 +2,13 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>BGTaskSchedulerPermittedIdentifiers</key>
+	<array>
+		<string>app.alextran.immich.backgroundFetch</string>
+		<string>app.alextran.immich.backgroundProcessing</string>
+	</array>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>
@@ -12,66 +19,6 @@
 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
 	<key>CFBundleInfoDictionaryVersion</key>
 	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>immich_mobile</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleShortVersionString</key>
-	<string>1.47.0</string>
-	<key>CFBundleSignature</key>
-	<string>????</string>
-	<key>CFBundleVersion</key>
-	<string>86</string>
-	<key>LSRequiresIPhoneOS</key>
-	<true/>
-	<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
-	<true/>
-	<key>NSAppTransportSecurity</key>
-	<dict>
-		<key>NSAllowsArbitraryLoads</key>
-		<true/>
-	</dict>
-	<key>NSLocationAlwaysUsageDescription</key>
-	<string>Enable location setting to show position of assets on map</string>
-	<key>NSLocationWhenInUseUsageDescription</key>
-	<string>Enable location setting to show position of assets on map</string>
-	<key>NSPhotoLibraryUsageDescription</key>
-	<string>We need to manage backup your photos album</string>
-	<key>NSPhotoLibraryAddUsageDescription</key>
-	<string>We need to manage backup your photos album</string>
-	<key>NSCameraUsageDescription</key>
-	<string>We need to access the camera to let you take beautiful video using this app</string>
-	<key>NSMicrophoneUsageDescription</key>
-	<string>We need to access the microphone to let you take beautiful video using this app</string>
-	<key>UILaunchStoryboardName</key>
-	<string>LaunchScreen</string>
-	<key>UIMainStoryboardFile</key>
-	<string>Main</string>
-	<key>UISupportedInterfaceOrientations</key>
-	<array>
-		<string>UIInterfaceOrientationPortrait</string>
-		<string>UIInterfaceOrientationLandscapeLeft</string>
-		<string>UIInterfaceOrientationLandscapeRight</string>
-	</array>
-	<key>UISupportedInterfaceOrientations~ipad</key>
-	<array>
-		<string>UIInterfaceOrientationPortrait</string>
-		<string>UIInterfaceOrientationPortraitUpsideDown</string>
-		<string>UIInterfaceOrientationLandscapeLeft</string>
-		<string>UIInterfaceOrientationLandscapeRight</string>
-	</array>
-	<key>UIViewControllerBasedStatusBarAppearance</key>
-	<true/>
-	<key>io.flutter.embedded_views_preview</key>
-	<true/>
-	<key>ITSAppUsesNonExemptEncryption</key>
-	<false/>
-	<key>CADisableMinimumFrameDurationOnPhone</key>
-	<true/>
-	<key>LSApplicationQueriesSchemes</key>
-	<array>
-		<string>https</string>
-	</array>
 	<key>CFBundleLocalizations</key>
 	<array>
 		<string>cs</string>
@@ -92,11 +39,74 @@
 		<string>sk</string>
 		<string>zh</string>
 	</array>
-	<key>UIStatusBarHidden</key>
+	<key>CFBundleName</key>
+	<string>immich_mobile</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.47.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>86</string>
+	<key>FLTEnableImpeller</key>
+	<true/>
+	<key>ITSAppUsesNonExemptEncryption</key>
 	<false/>
+	<key>LSApplicationQueriesSchemes</key>
+	<array>
+		<string>https</string>
+	</array>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
+	<true/>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
+	<key>NSCameraUsageDescription</key>
+	<string>We need to access the camera to let you take beautiful video using this app</string>
+	<key>NSLocationAlwaysUsageDescription</key>
+	<string>Enable location setting to show position of assets on map</string>
+	<key>NSLocationWhenInUseUsageDescription</key>
+	<string>Enable location setting to show position of assets on map</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>We need to access the microphone to let you take beautiful video using this app</string>
+	<key>NSPhotoLibraryAddUsageDescription</key>
+	<string>We need to manage backup your photos album</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>We need to manage backup your photos album</string>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
-	<key>FLTEnableImpeller</key>
+	<key>UIBackgroundModes</key>
+	<array>
+		<string>fetch</string>
+		<string>processing</string>
+	</array>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIStatusBarHidden</key>
+	<false/>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UIViewControllerBasedStatusBarAppearance</key>
+	<true/>
+	<key>io.flutter.embedded_views_preview</key>
 	<true/>
 </dict>
 </plist>
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index fe96a5d274..f85189358f 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -47,10 +47,6 @@ Future<void> openBoxes() async {
     Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
     Hive.openBox(hiveGithubReleaseInfoBox),
     Hive.openBox(userSettingInfoBox),
-    if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
-    if (!Platform.isAndroid)
-      Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
-    if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
     EasyLocalization.ensureInitialized(),
   ]);
 }
diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart
index 1e3e120851..59ad4dc604 100644
--- a/mobile/lib/modules/backup/background_service/background.service.dart
+++ b/mobile/lib/modules/backup/background_service/background.service.dart
@@ -2,7 +2,7 @@ import 'dart:async';
 import 'dart:developer';
 import 'dart:io';
 import 'dart:isolate';
-import 'dart:ui' show IsolateNameServer, PluginUtilities;
+import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
 import 'package:cancellation_token_http/http.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/services.dart';
@@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart';
 import 'package:hive_flutter/hive_flutter.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
-import 'package:immich_mobile/modules/backup/background_service/localization.dart';
 import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
 import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
 import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
@@ -19,6 +18,7 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:path_provider_ios/path_provider_ios.dart';
 import 'package:photo_manager/photo_manager.dart';
 
 final backgroundServiceProvider = Provider(
@@ -51,8 +51,7 @@ class BackgroundService {
   late final _Throttle _throttledDetailNotify =
       _Throttle(_updateDetailProgress, notifyInterval);
   Completer<bool> _hasAccessCompleter = Completer();
-  late Future<bool> _hasAccess =
-      Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
+  late Future<bool> _hasAccess = _hasAccessCompleter.future;
 
   Future<bool> get hasAccess => _hasAccess;
 
@@ -67,9 +66,6 @@ class BackgroundService {
 
   /// Enqueues the background service
   Future<bool> enableService({bool immediate = false}) async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
       final String title =
@@ -89,9 +85,6 @@ class BackgroundService {
     int triggerUpdateDelay = 5000,
     int triggerMaxDelay = 50000,
   }) async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       final bool ok = await _foregroundChannel.invokeMethod(
         'configure',
@@ -110,9 +103,6 @@ class BackgroundService {
 
   /// Cancels the background service (if currently running) and removes it from work queue
   Future<bool> disableService() async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       final ok = await _foregroundChannel.invokeMethod('disable');
       return ok;
@@ -123,9 +113,6 @@ class BackgroundService {
 
   /// Returns `true` if the background service is enabled
   Future<bool> isBackgroundBackupEnabled() async {
-    if (!Platform.isAndroid) {
-      return false;
-    }
     try {
       return await _foregroundChannel.invokeMethod("isEnabled");
     } catch (error) {
@@ -135,7 +122,8 @@ class BackgroundService {
 
   /// Returns `true` if battery optimizations are disabled
   Future<bool> isIgnoringBatteryOptimizations() async {
-    if (!Platform.isAndroid) {
+    // iOS does not need battery optimizations enabled
+    if (Platform.isIOS) {
       return true;
     }
     try {
@@ -156,9 +144,6 @@ class BackgroundService {
     bool isDetail = false,
     bool onlyIfFG = false,
   }) async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       if (_isBackgroundInitialized) {
         return _backgroundChannel.invokeMethod<bool>(
@@ -178,9 +163,6 @@ class BackgroundService {
     String? content,
     String? individualTag,
   }) async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
         return await _backgroundChannel
@@ -193,9 +175,6 @@ class BackgroundService {
   }
 
   Future<bool> _clearErrorNotifications() async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     try {
       if (_isBackgroundInitialized) {
         return await _backgroundChannel.invokeMethod('clearErrorNotifications');
@@ -210,9 +189,6 @@ class BackgroundService {
 
   /// await to ensure this thread (foreground or background) has exclusive access
   Future<bool> acquireLock() async {
-    if (!Platform.isAndroid) {
-      return true;
-    }
     if (_hasLock) {
       debugPrint("WARNING: [acquireLock] called more than once");
       return true;
@@ -253,7 +229,7 @@ class BackgroundService {
       while (_wantsLockTime == lockTime) {
         other.send(tempSp);
         final dynamic answer = await bs.first
-            .timeout(const Duration(seconds: 5), onTimeout: () => null);
+            .timeout(const Duration(seconds: 3), onTimeout: () => null);
         if (_wantsLockTime != lockTime) {
           break;
         }
@@ -270,7 +246,7 @@ class BackgroundService {
           // other isolate is still active
         }
         final dynamic isFinished = await bs.first
-            .timeout(const Duration(seconds: 5), onTimeout: () => false);
+            .timeout(const Duration(seconds: 3), onTimeout: () => false);
         if (isFinished == true) {
           break;
         }
@@ -288,9 +264,6 @@ class BackgroundService {
 
   /// releases the exclusive access lock
   void releaseLock() {
-    if (!Platform.isAndroid) {
-      return;
-    }
     _wantsLockTime = 0;
     if (_hasLock) {
       _hasAccessCompleter = Completer();
@@ -311,17 +284,35 @@ class BackgroundService {
   }
 
   Future<bool> _callHandler(MethodCall call) async {
+    DartPluginRegistrant.ensureInitialized();
+    if (Platform.isIOS) {
+      // NOTE: I'm not sure this is strictly necessary anymore, but
+      // out of an abundance of caution, we will keep it in until someone
+      // can say for sure
+      PathProviderIOS.registerWith();
+    }
     switch (call.method) {
+      case "backgroundProcessing":
       case "onAssetsChanged":
-        final Future<bool> translationsLoaded = loadTranslations();
         try {
           _clearErrorNotifications();
-          final bool hasAccess = await acquireLock();
+
+          // iOS should time out after some threshhold so it doesn't wait
+          // indefinitely and can run later
+          // Android is fine to wait here until the lock releases
+          final waitForLock = Platform.isIOS
+            ? acquireLock()
+              .timeout(
+                const Duration(seconds: 5),
+                onTimeout: () => false,
+              )
+            : acquireLock();
+
+          final bool hasAccess = await waitForLock;
           if (!hasAccess) {
             debugPrint("[_callHandler] could not acquire lock, exiting");
             return false;
           }
-          await translationsLoaded;
           final bool ok = await _onAssetsChanged();
           return ok;
         } catch (error) {
@@ -388,9 +379,9 @@ class BackgroundService {
             .put(backupFailedSince, DateTime.now());
         return false;
       }
-      // check for new assets added while performing backup
-    } while (true ==
-        await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
+      // Android should check for new assets added while performing backup
+    } while (Platform.isAndroid &&
+      true == await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
     return true;
   }
 
@@ -560,6 +551,28 @@ class BackgroundService {
     final String total = numberFormat.format(bytesTotal / 1024.0);
     return "$percent% ($done/$total$unit)";
   }
+
+  Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
+    // Seconds since last run
+    final double? lastRun = task == IosBackgroundTask.fetch
+      ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime')
+      : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime');
+    if (lastRun == null) {
+      return null;
+    }
+    final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000);
+    return time;
+  }
+
+  Future<int> getIOSBackupNumberOfProcesses() async {
+    return await _foregroundChannel
+      .invokeMethod('numberOfBackgroundProcesses');
+  }
+}
+
+enum IosBackgroundTask {
+  fetch,
+  processing
 }
 
 class _Throttle {
@@ -603,6 +616,7 @@ class _Throttle {
 @pragma('vm:entry-point')
 void _nativeEntry() {
   WidgetsFlutterBinding.ensureInitialized();
+  DartPluginRegistrant.ensureInitialized();
   BackgroundService backgroundService = BackgroundService();
   backgroundService._setupBackgroundCallHandler();
 }
diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart
index 38b4670b23..2e57250a06 100644
--- a/mobile/lib/modules/backup/providers/backup.provider.dart
+++ b/mobile/lib/modules/backup/providers/backup.provider.dart
@@ -1,5 +1,3 @@
-import 'dart:io';
-
 import 'package:cancellation_token_http/http.dart';
 import 'package:flutter/widgets.dart';
 import 'package:hive_flutter/hive_flutter.dart';
@@ -130,7 +128,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
           requireCharging != null ||
           triggerDelay != null,
     );
-    if (Platform.isAndroid) {
       final bool wasEnabled = state.backgroundBackup;
       final bool wasWifi = state.backupRequireWifi;
       final bool wasCharging = state.backupRequireCharging;
@@ -180,7 +177,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
           onError("backup_controller_page_background_configure_error");
         }
       }
-    }
   }
 
   ///
@@ -577,48 +573,47 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   }
 
   Future<void> resumeBackup() async {
-    if (Platform.isAndroid) {
-      // assumes the background service is currently running
-      // if true, waits until it has stopped to update the app state from HiveDB
-      // before actually resuming backup by calling the internal `_resumeBackup`
-      final BackUpProgressEnum previous = state.backupProgress;
-      state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
-      final bool hasLock = await _backgroundService.acquireLock();
-      if (!hasLock) {
-        log.warning("WARNING [resumeBackup] failed to acquireLock");
-        return;
-      }
-      await Future.wait([
-        Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
-        Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
-        Hive.openBox(backgroundBackupInfoBox),
-      ]);
-      final HiveBackupAlbums? albums =
-          Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
-      Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
-      Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
-      if (albums != null) {
-        selectedAlbums = _updateAlbumsBackupTime(
-          selectedAlbums,
-          albums.selectedAlbumIds,
-          albums.lastSelectedBackupTime,
-        );
-        excludedAlbums = _updateAlbumsBackupTime(
-          excludedAlbums,
-          albums.excludedAlbumsIds,
-          albums.lastExcludedBackupTime,
-        );
-      }
-      final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
-      state = state.copyWith(
-        backupProgress: previous,
-        selectedBackupAlbums: selectedAlbums,
-        excludedBackupAlbums: excludedAlbums,
-        backupRequireWifi: backgroundBox.get(backupRequireWifi),
-        backupRequireCharging: backgroundBox.get(backupRequireCharging),
-        backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
+
+    // assumes the background service is currently running
+    // if true, waits until it has stopped to update the app state from HiveDB
+    // before actually resuming backup by calling the internal `_resumeBackup`
+    final BackUpProgressEnum previous = state.backupProgress;
+    state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
+    final bool hasLock = await _backgroundService.acquireLock();
+    if (!hasLock) {
+      log.warning("WARNING [resumeBackup] failed to acquireLock");
+      return;
+    }
+    await Future.wait([
+      Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
+      Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
+      Hive.openBox(backgroundBackupInfoBox),
+    ]);
+    final HiveBackupAlbums? albums =
+        Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
+    Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
+    Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
+    if (albums != null) {
+      selectedAlbums = _updateAlbumsBackupTime(
+        selectedAlbums,
+        albums.selectedAlbumIds,
+        albums.lastSelectedBackupTime,
+      );
+      excludedAlbums = _updateAlbumsBackupTime(
+        excludedAlbums,
+        albums.excludedAlbumsIds,
+        albums.lastExcludedBackupTime,
       );
     }
+    final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
+    state = state.copyWith(
+      backupProgress: previous,
+      selectedBackupAlbums: selectedAlbums,
+      excludedBackupAlbums: excludedAlbums,
+      backupRequireWifi: backgroundBox.get(backupRequireWifi),
+      backupRequireCharging: backgroundBox.get(backupRequireCharging),
+      backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
+    );
     return _resumeBackup();
   }
 
@@ -649,8 +644,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       AppStateEnum.paused,
       AppStateEnum.detached,
     ];
-    if (Platform.isAndroid &&
-        allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
+    if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
       try {
         if (Hive.isBoxOpen(hiveBackupInfoBox)) {
           await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
@@ -683,6 +677,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       _backgroundService.releaseLock();
     }
   }
+
 }
 
 final backupProvider =
diff --git a/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart b/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
new file mode 100644
index 0000000000..5f796622a1
--- /dev/null
+++ b/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
+import 'package:intl/intl.dart';
+
+/// This is a simple debug widget which should be removed later on when we are
+/// more confident about background sync
+class IosDebugInfoTile extends HookConsumerWidget {
+  const IosDebugInfoTile({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final futures = [
+      ref.read(backgroundServiceProvider)
+        .getIOSBackupLastRun(IosBackgroundTask.fetch),
+      ref.read(backgroundServiceProvider)
+        .getIOSBackupLastRun(IosBackgroundTask.processing),
+      ref.read(backgroundServiceProvider)
+        .getIOSBackupNumberOfProcesses(),
+    ];
+    return FutureBuilder<List<dynamic>>(
+      future: Future.wait(futures),
+      builder: (context, snapshot) {
+        String? title;
+        String? subtitle;
+        if (snapshot.hasData) {
+          final results = snapshot.data as List<dynamic>;
+          final fetch = results[0] as DateTime?;
+          final processing = results[1] as DateTime?;
+          final processes = results[2] as int;
+
+          final processOrProcesses = processes == 1 ? 'process' : 'processes';
+          final numberOrZero = processes == 0 ? 'No' : processes.toString();
+          title = '$numberOrZero background $processOrProcesses queued';
+
+          final df = DateFormat.yMd().add_jm();
+          if (fetch == null && processing == null) {
+            subtitle = 'No background sync job has run yet';
+          } else if (fetch != null && processing == null) {
+            subtitle = 'Fetch ran ${df.format(fetch)}';
+          } else if (processing != null && fetch == null) {
+            subtitle = 'Processing ran ${df.format(processing)}';
+          } else {
+            final fetchOrProcessing = fetch!.isAfter(processing!)
+              ? fetch
+              : processing;
+            subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
+          }
+        }
+
+        return AnimatedSwitcher(
+          duration: const Duration(milliseconds: 200),
+          child: ListTile(
+            key: ValueKey(title),
+            title: Text(title ?? ''),
+            subtitle: Text(subtitle ?? ''),
+            leading: Icon(
+              Icons.bug_report,
+              color: Theme.of(context).primaryColor,
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
+
diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart
index 8fc9b035e5..6a950eb476 100644
--- a/mobile/lib/modules/backup/views/backup_controller_page.dart
+++ b/mobile/lib/modules/backup/views/backup_controller_page.dart
@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
 import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
+import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -195,7 +196,7 @@ class BackupControllerPage extends HookConsumerWidget {
       );
     }
 
-    ListTile buildBackgroundBackupController() {
+    Widget buildBackgroundBackupController() {
       final bool isBackgroundEnabled = backupState.backgroundBackup;
       final bool isWifiRequired = backupState.backupRequireWifi;
       final bool isChargingRequired = backupState.backupRequireCharging;
@@ -240,118 +241,126 @@ class BackupControllerPage extends HookConsumerWidget {
       final triggerDelay =
           useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
 
-      return ListTile(
-        isThreeLine: true,
-        leading: isBackgroundEnabled
-            ? Icon(
-                Icons.cloud_sync_rounded,
-                color: activeColor,
-              )
-            : const Icon(Icons.cloud_sync_rounded),
-        title: Text(
-          isBackgroundEnabled
-              ? "backup_controller_page_background_is_on"
-              : "backup_controller_page_background_is_off",
-          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
-        ).tr(),
-        subtitle: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            if (!isBackgroundEnabled)
-              Padding(
-                padding: const EdgeInsets.symmetric(vertical: 8.0),
-                child:
-                    const Text("backup_controller_page_background_description")
-                        .tr(),
-              ),
-            if (isBackgroundEnabled)
-              SwitchListTile(
-                title:
-                    const Text("backup_controller_page_background_wifi").tr(),
-                secondary: Icon(
-                  Icons.wifi,
-                  color: isWifiRequired ? activeColor : null,
-                ),
-                dense: true,
-                activeColor: activeColor,
-                value: isWifiRequired,
-                onChanged: hasExclusiveAccess
-                    ? (isChecked) => ref
-                        .read(backupProvider.notifier)
-                        .configureBackgroundBackup(
-                          requireWifi: isChecked,
-                          onError: showErrorToUser,
-                          onBatteryInfo: showBatteryOptimizationInfoToUser,
-                        )
-                    : null,
-              ),
-            if (isBackgroundEnabled)
-              SwitchListTile(
-                title: const Text("backup_controller_page_background_charging")
-                    .tr(),
-                secondary: Icon(
-                  Icons.charging_station,
-                  color: isChargingRequired ? activeColor : null,
-                ),
-                dense: true,
-                activeColor: activeColor,
-                value: isChargingRequired,
-                onChanged: hasExclusiveAccess
-                    ? (isChecked) => ref
-                        .read(backupProvider.notifier)
-                        .configureBackgroundBackup(
-                          requireCharging: isChecked,
-                          onError: showErrorToUser,
-                          onBatteryInfo: showBatteryOptimizationInfoToUser,
-                        )
-                    : null,
-              ),
-            if (isBackgroundEnabled)
-              ListTile(
-                isThreeLine: false,
-                dense: true,
-                enabled: hasExclusiveAccess,
-                title: const Text(
-                  'backup_controller_page_background_delay',
-                  style: TextStyle(
-                    fontWeight: FontWeight.bold,
+      return Column(
+        children: [
+          ListTile(
+            isThreeLine: true,
+            leading: isBackgroundEnabled
+                ? Icon(
+                    Icons.cloud_sync_rounded,
+                    color: activeColor,
+                  )
+                : const Icon(Icons.cloud_sync_rounded),
+            title: Text(
+              isBackgroundEnabled
+                  ? "backup_controller_page_background_is_on"
+                  : "backup_controller_page_background_is_off",
+              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+            ).tr(),
+            subtitle: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                if (!isBackgroundEnabled)
+                  Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 8.0),
+                    child:
+                        const Text("backup_controller_page_background_description")
+                            .tr(),
                   ),
-                ).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
-                subtitle: Slider(
-                  value: triggerDelay.value,
-                  onChanged: hasExclusiveAccess
-                      ? (double v) => triggerDelay.value = v
-                      : null,
-                  onChangeEnd: (double v) => ref
-                      .read(backupProvider.notifier)
-                      .configureBackgroundBackup(
-                        triggerDelay: backupDelayToMilliseconds(v),
-                        onError: showErrorToUser,
-                        onBatteryInfo: showBatteryOptimizationInfoToUser,
+                if (isBackgroundEnabled && Platform.isAndroid)
+                  SwitchListTile(
+                    title:
+                        const Text("backup_controller_page_background_wifi").tr(),
+                    secondary: Icon(
+                      Icons.wifi,
+                      color: isWifiRequired ? activeColor : null,
+                    ),
+                    dense: true,
+                    activeColor: activeColor,
+                    value: isWifiRequired,
+                    onChanged: hasExclusiveAccess
+                        ? (isChecked) => ref
+                            .read(backupProvider.notifier)
+                            .configureBackgroundBackup(
+                              requireWifi: isChecked,
+                              onError: showErrorToUser,
+                              onBatteryInfo: showBatteryOptimizationInfoToUser,
+                            )
+                        : null,
+                  ),
+                if (isBackgroundEnabled)
+                  SwitchListTile(
+                    title: const Text("backup_controller_page_background_charging")
+                        .tr(),
+                    secondary: Icon(
+                      Icons.charging_station,
+                      color: isChargingRequired ? activeColor : null,
+                    ),
+                    dense: true,
+                    activeColor: activeColor,
+                    value: isChargingRequired,
+                    onChanged: hasExclusiveAccess
+                        ? (isChecked) => ref
+                            .read(backupProvider.notifier)
+                            .configureBackgroundBackup(
+                              requireCharging: isChecked,
+                              onError: showErrorToUser,
+                              onBatteryInfo: showBatteryOptimizationInfoToUser,
+                            )
+                        : null,
+                  ),
+                if (isBackgroundEnabled && Platform.isAndroid)
+                  ListTile(
+                    isThreeLine: false,
+                    dense: true,
+                    enabled: hasExclusiveAccess,
+                    title: const Text(
+                      'backup_controller_page_background_delay',
+                      style: TextStyle(
+                        fontWeight: FontWeight.bold,
                       ),
-                  max: 3.0,
-                  divisions: 3,
-                  label: formatBackupDelaySliderValue(triggerDelay.value),
-                  activeColor: Theme.of(context).primaryColor,
+                    ).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
+                    subtitle: Slider(
+                      value: triggerDelay.value,
+                      onChanged: hasExclusiveAccess
+                          ? (double v) => triggerDelay.value = v
+                          : null,
+                      onChangeEnd: (double v) => ref
+                          .read(backupProvider.notifier)
+                          .configureBackgroundBackup(
+                            triggerDelay: backupDelayToMilliseconds(v),
+                            onError: showErrorToUser,
+                            onBatteryInfo: showBatteryOptimizationInfoToUser,
+                          ),
+                      max: 3.0,
+                      divisions: 3,
+                      label: formatBackupDelaySliderValue(triggerDelay.value),
+                      activeColor: Theme.of(context).primaryColor,
+                    ),
+                  ),
+                ElevatedButton(
+                  onPressed: () =>
+                      ref.read(backupProvider.notifier).configureBackgroundBackup(
+                            enabled: !isBackgroundEnabled,
+                            onError: showErrorToUser,
+                            onBatteryInfo: showBatteryOptimizationInfoToUser,
+                          ),
+                  child: Text(
+                    isBackgroundEnabled
+                        ? "backup_controller_page_background_turn_off"
+                        : "backup_controller_page_background_turn_on",
+                    style:
+                        const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
+                  ).tr(),
                 ),
-              ),
-            ElevatedButton(
-              onPressed: () =>
-                  ref.read(backupProvider.notifier).configureBackgroundBackup(
-                        enabled: !isBackgroundEnabled,
-                        onError: showErrorToUser,
-                        onBatteryInfo: showBatteryOptimizationInfoToUser,
-                      ),
-              child: Text(
-                isBackgroundEnabled
-                    ? "backup_controller_page_background_turn_off"
-                    : "backup_controller_page_background_turn_on",
-                style:
-                    const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
-              ).tr(),
+              ],
             ),
-          ],
-        ),
+          ),
+          if (isBackgroundEnabled)
+            IosDebugInfoTile(
+              key: ValueKey(isChargingRequired),
+            ),
+        ],
       );
     }
 
@@ -592,8 +601,8 @@ class BackupControllerPage extends HookConsumerWidget {
             ),
             const Divider(),
             buildAutoBackupController(),
-            if (Platform.isAndroid) const Divider(),
-            if (Platform.isAndroid) buildBackgroundBackupController(),
+            const Divider(),
+            buildBackgroundBackupController(),
             const Divider(),
             buildStorageInformation(),
             const Divider(),
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 9bf3e466e3..f2a9e074a8 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -165,10 +165,10 @@ packages:
     dependency: transitive
     description:
       name: cancellation_token
-      sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb"
+      sha256: e40ac742c7faac52e1719ce249934e20975e5772d40112e1e01cfc5abf24185a
       url: "https://pub.dev"
     source: hosted
-    version: "1.6.1"
+    version: "1.5.0"
   cancellation_token_http:
     dependency: "direct main"
     description:
@@ -333,10 +333,10 @@ packages:
     dependency: transitive
     description:
       name: fixnum
-      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.0"
+    version: "1.0.1"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -428,10 +428,10 @@ packages:
     dependency: transitive
     description:
       name: flutter_riverpod
-      sha256: "0c997763ce06359ee4686553b74def84062e9d6929ac63f61fa02465c1f8e32c"
+      sha256: "46a27b7a11dc13738054093076f2dc65692ddcd463979b15092accf5681aea20"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.2.0"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -483,10 +483,10 @@ packages:
     dependency: transitive
     description:
       name: glob
-      sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
+      sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.0"
   graphs:
     dependency: transitive
     description:
@@ -523,10 +523,10 @@ packages:
     dependency: "direct main"
     description:
       name: hooks_riverpod
-      sha256: "71695b2e1dfc22a39f1f9c67b798f8f8f1521f2d0349817d13ccdd5c4cd7acba"
+      sha256: a596bcb1eaf48eae6da1ce8b9e60ec9538ef7d15725e941c3626f29dfcc01d96
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.2.0"
   html:
     dependency: transitive
     description:
@@ -851,6 +851,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
+  path_provider_ios:
+    dependency: "direct main"
+    description:
+      name: path_provider_ios
+      sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.11"
   path_provider_linux:
     dependency: transitive
     description:
@@ -975,10 +983,10 @@ packages:
     dependency: transitive
     description:
       name: riverpod
-      sha256: "0f43c64f1f79c2112c843305a879a746587fb7c1e388f1d4717737796756e2c4"
+      sha256: "59a48de9c757aa61aa28e9fd625ffb360d43b6b54606f12536622c55be9e8c4b"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.2.0"
   rxdart:
     dependency: transitive
     description:
@@ -1492,10 +1500,10 @@ packages:
     dependency: transitive
     description:
       name: xml
-      sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
+      sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb
       url: "https://pub.dev"
     source: hosted
-    version: "6.2.2"
+    version: "6.1.0"
   xxh3:
     dependency: transitive
     description:
@@ -1513,5 +1521,5 @@ packages:
     source: hosted
     version: "3.1.1"
 sdks:
-  dart: ">=2.19.0 <4.0.0"
+  dart: ">=2.18.0 <3.0.0"
   flutter: ">=3.3.0"
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 5380b194b3..648c53b1b3 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -12,6 +12,7 @@ dependencies:
   flutter:
     sdk: flutter
 
+  path_provider_ios:
   photo_manager: ^2.5.0
   flutter_hooks: ^0.18.0
   hooks_riverpod: ^2.0.0-dev.0