diff --git a/README.md b/README.md
index 94c8db3..39b6135 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Bedrock is designed to be the foundation upon which apps are built, providing:
| **Theme Guide** | [`Sources/Bedrock/Theme/THEME_GUIDE.md`](Sources/Bedrock/Theme/THEME_GUIDE.md) | Create custom color themes using Bedrock's protocol-based theming system |
| **Branding Guide** | [`Sources/Bedrock/Branding/BRANDING_GUIDE.md`](Sources/Bedrock/Branding/BRANDING_GUIDE.md) | Set up app icons, launch screens, and branded launch animations |
| **Settings Guide** | [`Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md`](Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md) | Build branded settings screens with reusable UI components |
+| **SwiftData CloudKit Guide** | [`Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md`](Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md) | Configure SwiftData+iCloud sync with reusable Bedrock sync lifecycle utilities |
### Implementation Checklist
@@ -76,6 +77,15 @@ Use this checklist when setting up a new app with Bedrock:
- [ ] Avoid manual horizontal child padding inside `SettingsCard` (card owns row insets)
- [ ] Add `#if DEBUG` section for development tools
+#### ☁️ SwiftData + CloudKit Sync (When enabling cross-device data sync)
+- [ ] **Read**: `SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md`
+- [ ] Enable iCloud/CloudKit + Push Notifications + Remote notifications background mode
+- [ ] Configure `aps-environment` via xcconfig (`development`/`production`)
+- [ ] Wire `SwiftDataCloudKitSyncManager` into app store lifecycle
+- [ ] Use `SwiftDataRefreshThrottler` for minimum-interval refresh gates
+- [ ] Add a root-level refresh version observation for reliable UI invalidation
+- [ ] Validate with two physical devices and CloudKit Console checks
+
### Quick Start Order
When building a new app from scratch:
diff --git a/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md b/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md
new file mode 100644
index 0000000..3ec7ab4
--- /dev/null
+++ b/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md
@@ -0,0 +1,171 @@
+# SwiftData + CloudKit Setup Guide
+
+Use this guide when enabling near-real-time SwiftData sync across multiple Apple devices.
+
+## Overview
+
+Bedrock provides four reusable building blocks for SwiftData + CloudKit apps:
+
+- `SwiftDataCloudKitSyncManager`: observes remote CloudKit-backed store changes.
+- `SwiftDataRefreshThrottler`: enforces minimum refresh intervals.
+- `SwiftDataStore`: generic store protocol (`modelContext`, `modelContainer`, `reloadData`).
+- `SwiftDataCloudKitStore`: adds CloudKit sync defaults (remote-change metadata + Mac pulse fallback).
+
+These utilities are domain-neutral and intended to be integrated by your app store/state layer.
+
+## 1) Required Capabilities and Entitlements
+
+Enable in the app target:
+
+- iCloud with CloudKit
+- Push Notifications
+- Background Modes -> Remote notifications
+- App Groups (if app/widget share SQLite storage)
+
+Required entitlements:
+
+- `com.apple.developer.icloud-container-identifiers`
+- `com.apple.developer.icloud-services` includes `CloudKit`
+- `aps-environment` (per build configuration)
+- `com.apple.security.application-groups` (if app group is used)
+
+Recommended xcconfig setup:
+
+```xcconfig
+// Debug.xcconfig
+APS_ENVIRONMENT = development
+
+// Release.xcconfig
+APS_ENVIRONMENT = production
+```
+
+Entitlements reference:
+
+```xml
+aps-environment
+$(APS_ENVIRONMENT)
+```
+
+## 2) Container Configuration
+
+Configure your SwiftData container with CloudKit mirroring:
+
+```swift
+let config = ModelConfiguration(
+ schema: schema,
+ url: storeURL,
+ cloudKitDatabase: isRunningTests ? .none : .private(cloudKitContainerIdentifier)
+)
+```
+
+For deterministic tests, disable CloudKit mirroring.
+
+## 3) Integrate `SwiftDataCloudKitSyncManager`
+
+Your app store owns domain behavior and plugs remote-change events into reload logic:
+
+```swift
+import Bedrock
+import SwiftData
+
+@MainActor
+@Observable
+final class AppDataStore: SwiftDataCloudKitStore {
+ @ObservationIgnored let modelContainer: ModelContainer
+ @ObservationIgnored var modelContext: ModelContext
+ @ObservationIgnored let cloudKitSyncManager = SwiftDataCloudKitSyncManager(
+ isEnabled: true,
+ logIdentifier: "AppCloudKitSync"
+ )
+
+ private(set) var refreshVersion: Int = 0
+
+ init(modelContext: ModelContext) {
+ self.modelContainer = modelContext.container
+ self.modelContext = modelContext
+ startSyncObservation()
+ reloadData()
+ }
+
+ private func startSyncObservation() {
+ cloudKitSyncManager.startObserving { [weak self] in
+ guard let self else { return }
+ _ = self.cloudKitSyncManager.processObservedRemoteChange(
+ modelContext: &self.modelContext,
+ modelContainer: self.modelContainer
+ )
+ self.reloadData()
+ }
+ }
+
+ func reloadData() {
+ // Fetch your app entities and recompute derived state.
+ refreshVersion &+= 1
+ }
+}
+```
+
+## 4) Integrate `SwiftDataRefreshThrottler`
+
+Use throttling for on-appear and tab/scene refresh paths:
+
+```swift
+@ObservationIgnored private let refreshThrottler = SwiftDataRefreshThrottler()
+
+func refreshIfNeeded(minimumInterval: TimeInterval = 5) {
+ guard refreshThrottler.shouldRefresh(minimumInterval: minimumInterval) else { return }
+ refresh()
+}
+```
+
+## 5) UI Invalidation Pattern
+
+Remote merges can arrive in bursts. Keep a refresh counter in your store and observe it in root-level views:
+
+```swift
+let _ = store.refreshVersion
+```
+
+This forces reevaluation even when model object identities do not change as expected.
+
+## 6) Fallback Refresh Pattern
+
+Keep a foreground fallback only as a safety net:
+
+1. Capture activation timestamp.
+2. Delay once (for example 8 seconds).
+3. Skip fallback if `store.hasReceivedRemoteChange(since:)` is true.
+4. Otherwise run a refresh.
+
+### Mac Runtime Fallback (iOS-on-Mac)
+
+If APNs delivery is unreliable on Mac runtime, pulse import scheduling explicitly while active:
+
+```swift
+if store.supportsMacCloudKitImportPulseFallback {
+ store.forceCloudKitImportPulse(reason: "active_loop")
+}
+```
+
+Recommended usage:
+
+1. Start a foreground-only task when scene becomes `.active`.
+2. Pulse on an interval (for example 5 seconds).
+3. Cancel on `.background` / `.inactive`.
+4. Keep this fallback Mac-only; leave iPhone/iPad on event-driven sync.
+
+## 7) Verification Checklist (Release Gate)
+
+Run on two physical devices, same Apple ID, same app flavor:
+
+1. Single edit appears cross-device while both apps are open.
+2. Rapid batch edits all appear without manual pull-to-refresh.
+3. Background/foreground recovery works without force-quit.
+4. Logs show remote event sequence increasing.
+5. CloudKit Console private DB records update as expected.
+
+## 8) Troubleshooting
+
+- Push events appear but UI is stale: verify context recreation + UI invalidation counter.
+- Updates only after app relaunch: verify remote observer active and push entitlement provisioning.
+- Works only in one build type: verify `aps-environment` and signing profile consistency.
diff --git a/Sources/Bedrock/Storage/SwiftDataCloudKitSyncManager.swift b/Sources/Bedrock/Storage/SwiftDataCloudKitSyncManager.swift
new file mode 100644
index 0000000..d7ca41f
--- /dev/null
+++ b/Sources/Bedrock/Storage/SwiftDataCloudKitSyncManager.swift
@@ -0,0 +1,182 @@
+//
+// SwiftDataCloudKitSyncManager.swift
+// Bedrock
+//
+// Reusable remote-change observer for SwiftData + CloudKit mirroring.
+//
+
+import Foundation
+import CoreData
+import SwiftData
+import SwiftUI
+
+@MainActor
+@Observable
+public final class SwiftDataCloudKitSyncManager {
+ public struct RemoteChangeProcessingResult {
+ public let eventCount: Int
+ public let lastRemoteChangeDate: Date?
+ public let didRebuildModelContext: Bool
+
+ public init(
+ eventCount: Int,
+ lastRemoteChangeDate: Date?,
+ didRebuildModelContext: Bool
+ ) {
+ self.eventCount = eventCount
+ self.lastRemoteChangeDate = lastRemoteChangeDate
+ self.didRebuildModelContext = didRebuildModelContext
+ }
+ }
+
+ /// Last timestamp when a remote store change was observed.
+ public private(set) var lastRemoteChangeDate: Date?
+
+ /// Monotonic count of observed remote store change events.
+ public private(set) var remoteChangeEventCount: Int = 0
+
+ /// Whether remote change observation is currently active.
+ public private(set) var isObservingRemoteChanges: Bool = false
+
+ private let isEnabled: Bool
+ private let logIdentifier: String
+ private var remoteChangeObserver: NSObjectProtocol?
+
+ public init(
+ isEnabled: Bool = true,
+ logIdentifier: String = "SwiftDataCloudKitSyncManager"
+ ) {
+ self.isEnabled = isEnabled
+ self.logIdentifier = logIdentifier
+ }
+
+ /// Starts observing Core Data remote store changes and invokes `onRemoteChange` per event.
+ public func startObserving(onRemoteChange: @escaping @MainActor () -> Void) {
+ guard isEnabled else {
+ Design.debugLog("\(logIdentifier): remote observation disabled")
+ return
+ }
+ guard remoteChangeObserver == nil else { return }
+
+ Design.debugLog("\(logIdentifier): starting remote store change observation")
+ isObservingRemoteChanges = true
+
+ remoteChangeObserver = NotificationCenter.default.addObserver(
+ forName: .NSPersistentStoreRemoteChange,
+ object: nil,
+ queue: .main
+ ) { [weak self] _ in
+ guard let self else { return }
+ self.remoteChangeEventCount += 1
+ self.lastRemoteChangeDate = Date()
+ onRemoteChange()
+ }
+ }
+
+ /// Stops observing remote store changes.
+ public func stopObserving() {
+ if let observer = remoteChangeObserver {
+ NotificationCenter.default.removeObserver(observer)
+ remoteChangeObserver = nil
+ }
+ isObservingRemoteChanges = false
+ }
+
+ /// Returns true when a remote store event has been observed on/after `date`.
+ public func hasReceivedRemoteChange(since date: Date) -> Bool {
+ guard let lastRemoteChangeDate else { return false }
+ return lastRemoteChangeDate >= date
+ }
+
+ /// Applies standard post-observation handling for a remote CloudKit merge.
+ ///
+ /// Call this from the `startObserving` callback before fetching data if your store
+ /// keeps a long-lived `ModelContext`.
+ @discardableResult
+ public func processObservedRemoteChange(
+ modelContext: inout ModelContext,
+ modelContainer: ModelContainer,
+ rebuildContextIfSafe: Bool = true
+ ) -> RemoteChangeProcessingResult {
+ var didRebuildModelContext = false
+
+ if rebuildContextIfSafe {
+ if modelContext.hasChanges {
+ Design.debugLog("\(logIdentifier): context has unsaved changes, skipping context rebuild after remote change")
+ } else {
+ modelContext = ModelContext(modelContainer)
+ didRebuildModelContext = true
+ Design.debugLog("\(logIdentifier): rebuilt model context after remote change")
+ }
+ }
+
+ return RemoteChangeProcessingResult(
+ eventCount: remoteChangeEventCount,
+ lastRemoteChangeDate: lastRemoteChangeDate,
+ didRebuildModelContext: didRebuildModelContext
+ )
+ }
+
+ /// Returns true when this runtime can use AppKit activation pulse fallback.
+ public var supportsMacImportPulseFallback: Bool {
+ ProcessInfo.processInfo.isiOSAppOnMac
+ }
+
+ /// Posts AppKit activation notifications that can nudge CloudKit import scheduling on iOS-on-Mac.
+ ///
+ /// Returns true only when a pulse was posted on a supported runtime.
+ @discardableResult
+ public func triggerMacImportPulse(reason: String? = nil) -> Bool {
+ guard supportsMacImportPulseFallback else { return false }
+
+ let reasonText = reason?.isEmpty == false ? reason! : "unspecified"
+ Design.debugLog("\(logIdentifier): triggering mac import pulse; reason=\(reasonText)")
+
+ let nsApp = NSClassFromString("NSApplication")?.value(forKeyPath: "sharedApplication") as? NSObject
+ Design.debugLog("\(logIdentifier): NSApplication.shared resolved=\(nsApp != nil)")
+
+ NotificationCenter.default.post(
+ name: Notification.Name("NSApplicationDidBecomeActiveNotification"),
+ object: nsApp
+ )
+ NotificationCenter.default.post(
+ name: Notification.Name("NSApplicationWillBecomeActiveNotification"),
+ object: nsApp
+ )
+
+ // Best-effort distributed notification for runtimes that rely on cross-process signaling.
+ if let distClass = NSClassFromString("NSDistributedNotificationCenter"),
+ let center = distClass.value(forKeyPath: "defaultCenter") as? NSObject {
+ _ = center.perform(
+ NSSelectorFromString("postNotificationName:object:"),
+ with: "NSApplicationDidBecomeActiveNotification",
+ with: nil
+ )
+ }
+
+ return true
+ }
+
+ /// Triggers the mac import pulse and refreshes `modelContext` when safe.
+ ///
+ /// Use this to avoid duplicating "pulse + context rebuild" boilerplate in each app store.
+ @discardableResult
+ public func triggerMacImportPulse(
+ reason: String? = nil,
+ modelContext: inout ModelContext,
+ modelContainer: ModelContainer,
+ rebuildContextIfSafe: Bool = true
+ ) -> Bool {
+ guard triggerMacImportPulse(reason: reason) else { return false }
+ guard rebuildContextIfSafe else { return true }
+
+ if modelContext.hasChanges {
+ Design.debugLog("\(logIdentifier): context has unsaved changes, skipping context rebuild after pulse")
+ return true
+ }
+
+ modelContext = ModelContext(modelContainer)
+ Design.debugLog("\(logIdentifier): rebuilt model context after pulse")
+ return true
+ }
+}
diff --git a/Sources/Bedrock/Storage/SwiftDataRefreshThrottler.swift b/Sources/Bedrock/Storage/SwiftDataRefreshThrottler.swift
new file mode 100644
index 0000000..016f19d
--- /dev/null
+++ b/Sources/Bedrock/Storage/SwiftDataRefreshThrottler.swift
@@ -0,0 +1,38 @@
+//
+// SwiftDataRefreshThrottler.swift
+// Bedrock
+//
+// Generic minimum-interval gate for refresh operations.
+//
+
+import Foundation
+
+public final class SwiftDataRefreshThrottler {
+ /// Timestamp of the last allowed refresh.
+ public private(set) var lastRefreshDate: Date?
+
+ public init(lastRefreshDate: Date? = nil) {
+ self.lastRefreshDate = lastRefreshDate
+ }
+
+ /// Returns true when a refresh should run now.
+ /// If true, records `now` as the latest refresh date.
+ @discardableResult
+ public func shouldRefresh(
+ now: Date = Date(),
+ minimumInterval: TimeInterval
+ ) -> Bool {
+ if let lastRefreshDate,
+ now.timeIntervalSince(lastRefreshDate) < minimumInterval {
+ return false
+ }
+
+ lastRefreshDate = now
+ return true
+ }
+
+ /// Clears refresh history so the next check can pass immediately.
+ public func reset() {
+ lastRefreshDate = nil
+ }
+}
diff --git a/Sources/Bedrock/Storage/SwiftDataStore.swift b/Sources/Bedrock/Storage/SwiftDataStore.swift
new file mode 100644
index 0000000..cfeb0f0
--- /dev/null
+++ b/Sources/Bedrock/Storage/SwiftDataStore.swift
@@ -0,0 +1,50 @@
+//
+// SwiftDataStore.swift
+// Bedrock
+//
+// Generic macOS fallback pulse wiring for SwiftData + CloudKit stores.
+//
+
+import Foundation
+import SwiftData
+
+@MainActor
+public protocol SwiftDataStore: AnyObject {
+ var modelContext: ModelContext { get set }
+ var modelContainer: ModelContainer { get }
+
+ /// App-specific fetch/recompute entry point.
+ func reloadData()
+}
+
+@MainActor
+public protocol SwiftDataCloudKitStore: SwiftDataStore {
+ var cloudKitSyncManager: SwiftDataCloudKitSyncManager { get }
+}
+
+@available(*, deprecated, renamed: "SwiftDataCloudKitStore")
+public typealias SwiftDataCloudKitPulseReloadingStore = SwiftDataCloudKitStore
+
+public extension SwiftDataCloudKitStore {
+ var lastRemoteChangeDate: Date? {
+ cloudKitSyncManager.lastRemoteChangeDate
+ }
+
+ var supportsMacCloudKitImportPulseFallback: Bool {
+ cloudKitSyncManager.supportsMacImportPulseFallback
+ }
+
+ func hasReceivedRemoteChange(since date: Date) -> Bool {
+ cloudKitSyncManager.hasReceivedRemoteChange(since: date)
+ }
+
+ /// Production fallback: pulses CloudKit import scheduling while app is active on Mac runtime.
+ func forceCloudKitImportPulse(reason: String) {
+ guard cloudKitSyncManager.triggerMacImportPulse(
+ reason: reason,
+ modelContext: &modelContext,
+ modelContainer: modelContainer
+ ) else { return }
+ reloadData()
+ }
+}