@zigrivers/scaffold 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +113 -8
  2. package/content/knowledge/browser-extension/browser-extension-architecture.md +195 -0
  3. package/content/knowledge/browser-extension/browser-extension-content-scripts.md +264 -0
  4. package/content/knowledge/browser-extension/browser-extension-conventions.md +156 -0
  5. package/content/knowledge/browser-extension/browser-extension-cross-browser.md +229 -0
  6. package/content/knowledge/browser-extension/browser-extension-dev-environment.md +247 -0
  7. package/content/knowledge/browser-extension/browser-extension-manifest.md +220 -0
  8. package/content/knowledge/browser-extension/browser-extension-project-structure.md +183 -0
  9. package/content/knowledge/browser-extension/browser-extension-requirements.md +107 -0
  10. package/content/knowledge/browser-extension/browser-extension-security.md +202 -0
  11. package/content/knowledge/browser-extension/browser-extension-service-workers.md +265 -0
  12. package/content/knowledge/browser-extension/browser-extension-store-submission.md +155 -0
  13. package/content/knowledge/browser-extension/browser-extension-testing.md +270 -0
  14. package/content/knowledge/data-pipeline/data-pipeline-architecture.md +175 -0
  15. package/content/knowledge/data-pipeline/data-pipeline-batch-patterns.md +263 -0
  16. package/content/knowledge/data-pipeline/data-pipeline-conventions.md +176 -0
  17. package/content/knowledge/data-pipeline/data-pipeline-dev-environment.md +350 -0
  18. package/content/knowledge/data-pipeline/data-pipeline-orchestration.md +291 -0
  19. package/content/knowledge/data-pipeline/data-pipeline-project-structure.md +257 -0
  20. package/content/knowledge/data-pipeline/data-pipeline-quality.md +324 -0
  21. package/content/knowledge/data-pipeline/data-pipeline-requirements.md +145 -0
  22. package/content/knowledge/data-pipeline/data-pipeline-schema-management.md +295 -0
  23. package/content/knowledge/data-pipeline/data-pipeline-security.md +326 -0
  24. package/content/knowledge/data-pipeline/data-pipeline-streaming-patterns.md +280 -0
  25. package/content/knowledge/data-pipeline/data-pipeline-testing.md +406 -0
  26. package/content/knowledge/library/library-api-design.md +306 -0
  27. package/content/knowledge/library/library-architecture.md +247 -0
  28. package/content/knowledge/library/library-bundling.md +244 -0
  29. package/content/knowledge/library/library-conventions.md +229 -0
  30. package/content/knowledge/library/library-dev-environment.md +220 -0
  31. package/content/knowledge/library/library-documentation.md +300 -0
  32. package/content/knowledge/library/library-project-structure.md +237 -0
  33. package/content/knowledge/library/library-requirements.md +173 -0
  34. package/content/knowledge/library/library-security.md +257 -0
  35. package/content/knowledge/library/library-testing.md +319 -0
  36. package/content/knowledge/library/library-type-definitions.md +284 -0
  37. package/content/knowledge/library/library-versioning.md +300 -0
  38. package/content/knowledge/ml/ml-architecture.md +172 -0
  39. package/content/knowledge/ml/ml-conventions.md +209 -0
  40. package/content/knowledge/ml/ml-dev-environment.md +299 -0
  41. package/content/knowledge/ml/ml-experiment-tracking.md +285 -0
  42. package/content/knowledge/ml/ml-model-evaluation.md +256 -0
  43. package/content/knowledge/ml/ml-observability.md +253 -0
  44. package/content/knowledge/ml/ml-project-structure.md +216 -0
  45. package/content/knowledge/ml/ml-requirements.md +138 -0
  46. package/content/knowledge/ml/ml-security.md +188 -0
  47. package/content/knowledge/ml/ml-serving-patterns.md +243 -0
  48. package/content/knowledge/ml/ml-testing.md +301 -0
  49. package/content/knowledge/ml/ml-training-patterns.md +269 -0
  50. package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
  51. package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
  52. package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
  53. package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
  54. package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
  55. package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
  56. package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
  57. package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
  58. package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
  59. package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
  60. package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
  61. package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
  62. package/content/methodology/browser-extension-overlay.yml +82 -0
  63. package/content/methodology/data-pipeline-overlay.yml +70 -0
  64. package/content/methodology/library-overlay.yml +67 -0
  65. package/content/methodology/ml-overlay.yml +70 -0
  66. package/content/methodology/mobile-app-overlay.yml +71 -0
  67. package/dist/cli/commands/init.d.ts +22 -0
  68. package/dist/cli/commands/init.d.ts.map +1 -1
  69. package/dist/cli/commands/init.js +202 -3
  70. package/dist/cli/commands/init.js.map +1 -1
  71. package/dist/cli/commands/init.test.js +190 -0
  72. package/dist/cli/commands/init.test.js.map +1 -1
  73. package/dist/config/schema.d.ts +1456 -80
  74. package/dist/config/schema.d.ts.map +1 -1
  75. package/dist/config/schema.js +87 -0
  76. package/dist/config/schema.js.map +1 -1
  77. package/dist/config/schema.test.js +312 -3
  78. package/dist/config/schema.test.js.map +1 -1
  79. package/dist/core/assembly/overlay-loader.test.js +55 -0
  80. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  81. package/dist/e2e/project-type-overlays.test.d.ts +2 -1
  82. package/dist/e2e/project-type-overlays.test.d.ts.map +1 -1
  83. package/dist/e2e/project-type-overlays.test.js +780 -14
  84. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  85. package/dist/types/config.d.ts +16 -1
  86. package/dist/types/config.d.ts.map +1 -1
  87. package/dist/wizard/questions.d.ts +28 -1
  88. package/dist/wizard/questions.d.ts.map +1 -1
  89. package/dist/wizard/questions.js +127 -1
  90. package/dist/wizard/questions.js.map +1 -1
  91. package/dist/wizard/questions.test.js +224 -4
  92. package/dist/wizard/questions.test.js.map +1 -1
  93. package/dist/wizard/wizard.d.ts +22 -0
  94. package/dist/wizard/wizard.d.ts.map +1 -1
  95. package/dist/wizard/wizard.js +28 -1
  96. package/dist/wizard/wizard.js.map +1 -1
  97. package/package.json +1 -1
@@ -0,0 +1,317 @@
1
+ ---
2
+ name: mobile-app-observability
3
+ description: Crash reporting (Crashlytics/Sentry), analytics, performance monitoring, network tracing, and structured logging for mobile apps
4
+ topics: [mobile-app, observability, crashlytics, sentry, analytics, performance-monitoring, network-tracing, logging]
5
+ ---
6
+
7
+ Mobile observability is harder than server observability: you cannot SSH into a user's phone, crashes happen on thousands of device/OS combinations you cannot reproduce locally, and performance issues manifest differently across network conditions and hardware tiers. The goal is to know about problems before users report them, understand why they occurred, and have enough context to reproduce and fix them.
8
+
9
+ ## Summary
10
+
11
+ Mobile observability requires crash reporting (Crashlytics or Sentry), structured analytics for user behavior and funnel tracking, real-user performance monitoring (launch time, screen transitions, network requests), and structured logging that survives app termination. Crashlytics and Sentry both capture symbolicated stack traces — symbolication requires uploading dSYMs (iOS) or mapping files (Android) in CI. Analytics events must be defined in a taxonomy before implementation. Performance monitoring should cover app start time, screen render time, and API latency percentiles.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Crash Reporting
16
+
17
+ **Firebase Crashlytics setup**
18
+
19
+ iOS (SPM):
20
+ ```swift
21
+ // AppDelegate
22
+ import FirebaseCrashlytics
23
+ import FirebaseCore
24
+
25
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
26
+ FirebaseApp.configure()
27
+ return true
28
+ }
29
+ ```
30
+
31
+ Android (Gradle):
32
+ ```kotlin
33
+ // app/build.gradle.kts
34
+ plugins {
35
+ id("com.google.firebase.crashlytics")
36
+ }
37
+ dependencies {
38
+ implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
39
+ implementation("com.google.firebase:firebase-crashlytics")
40
+ }
41
+ ```
42
+
43
+ **dSYM upload (iOS)**
44
+ Without dSYMs, crash reports show memory addresses, not function names. Crashlytics auto-uploads dSYMs when the Crashlytics build phase is configured:
45
+
46
+ ```bash
47
+ # Xcode build phase: Run Script
48
+ "${PODS_ROOT}/FirebaseCrashlytics/run"
49
+ ```
50
+
51
+ For Bitcode-disabled builds (Xcode 14+ default), dSYMs are generated at build time and uploaded automatically if the build phase is present. For CI builds:
52
+ ```bash
53
+ # Upload dSYMs manually after archiving
54
+ ./Pods/FirebaseCrashlytics/upload-symbols -gsp GoogleService-Info.plist -p ios path/to/dSYMs
55
+ ```
56
+
57
+ **Mapping file upload (Android)**
58
+ ```kotlin
59
+ // app/build.gradle.kts
60
+ buildTypes {
61
+ release {
62
+ firebaseCrashlytics {
63
+ mappingFileUploadEnabled = true // uploads R8/ProGuard mapping file automatically
64
+ nativeSymbolUploadEnabled = true // for NDK crash symbolication
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ **Enriching crash reports**
71
+ ```swift
72
+ // iOS: Add user context and breadcrumbs
73
+ Crashlytics.crashlytics().setUserID(userId)
74
+ Crashlytics.crashlytics().setCustomValue("premium", forKey: "subscription_tier")
75
+
76
+ // Record non-fatal errors
77
+ Crashlytics.crashlytics().record(error: networkError)
78
+
79
+ // Add breadcrumb before risky operation
80
+ Crashlytics.crashlytics().log("Attempting payment with method: \(paymentMethod)")
81
+ ```
82
+
83
+ ```kotlin
84
+ // Android
85
+ FirebaseCrashlytics.getInstance().setUserId(userId)
86
+ FirebaseCrashlytics.getInstance().setCustomKey("subscription_tier", "premium")
87
+ FirebaseCrashlytics.getInstance().recordException(exception)
88
+ FirebaseCrashlytics.getInstance().log("Cart checkout started: items=${cartItems.size}")
89
+ ```
90
+
91
+ **Sentry as alternative**
92
+ Sentry provides richer error grouping, performance tracing in the same SDK, and self-hosting options:
93
+
94
+ ```swift
95
+ // iOS
96
+ import Sentry
97
+ SentrySDK.start { options in
98
+ options.dsn = "https://key@sentry.io/project"
99
+ options.tracesSampleRate = 0.2 // 20% of sessions traced
100
+ options.profilesSampleRate = 0.1 // 10% of transactions profiled
101
+ options.enableCrashHandler = true
102
+ }
103
+ ```
104
+
105
+ ```kotlin
106
+ // Android
107
+ SentryAndroid.init(context) { options ->
108
+ options.dsn = "https://key@sentry.io/project"
109
+ options.tracesSampleRate = 0.2
110
+ options.isEnableAutoActivityLifecycleBreadcrumbs = true
111
+ }
112
+ ```
113
+
114
+ ### Analytics
115
+
116
+ **Event taxonomy design**
117
+ Define events before implementation. Use a consistent naming convention and document in a schema registry:
118
+
119
+ ```
120
+ Event naming: {object}_{action}
121
+ Examples:
122
+ user_signed_up
123
+ user_signed_in
124
+ product_viewed
125
+ product_added_to_cart
126
+ cart_checkout_started
127
+ order_placed
128
+ order_cancelled
129
+ ```
130
+
131
+ Event properties follow snake_case:
132
+ ```
133
+ product_viewed:
134
+ product_id: string
135
+ product_name: string
136
+ category: string
137
+ price_cents: int
138
+ position_in_list: int # for recommendation tracking
139
+ source: string # "search" | "recommendation" | "category_browse"
140
+ ```
141
+
142
+ **Firebase Analytics (iOS)**
143
+ ```swift
144
+ import FirebaseAnalytics
145
+
146
+ Analytics.logEvent("product_viewed", parameters: [
147
+ "product_id": productId,
148
+ "product_name": productName,
149
+ "price_cents": priceCents,
150
+ "source": source
151
+ ])
152
+
153
+ // User properties (persistent, applied to all subsequent events)
154
+ Analytics.setUserProperty("premium", forName: "subscription_tier")
155
+ ```
156
+
157
+ **Firebase Analytics (Android)**
158
+ ```kotlin
159
+ firebaseAnalytics.logEvent("product_viewed") {
160
+ param("product_id", productId)
161
+ param("product_name", productName)
162
+ param("price_cents", priceCents.toLong())
163
+ param("source", source)
164
+ }
165
+
166
+ firebaseAnalytics.setUserProperty("subscription_tier", "premium")
167
+ ```
168
+
169
+ **Analytics abstraction layer**
170
+ Never call analytics SDKs directly from feature code — use an abstraction:
171
+
172
+ ```swift
173
+ protocol AnalyticsService {
174
+ func track(_ event: AnalyticsEvent)
175
+ func setUserProperty(_ value: String, for key: String)
176
+ }
177
+
178
+ enum AnalyticsEvent {
179
+ case productViewed(productId: String, name: String, priceCents: Int, source: String)
180
+ case orderPlaced(orderId: String, itemCount: Int, totalCents: Int)
181
+ }
182
+
183
+ final class FirebaseAnalyticsService: AnalyticsService {
184
+ func track(_ event: AnalyticsEvent) {
185
+ switch event {
186
+ case .productViewed(let id, let name, let price, let source):
187
+ Analytics.logEvent("product_viewed", parameters: [
188
+ "product_id": id, "product_name": name,
189
+ "price_cents": price, "source": source
190
+ ])
191
+ case .orderPlaced(let id, let count, let total):
192
+ Analytics.logEvent("order_placed", parameters: [
193
+ "order_id": id, "item_count": count, "total_cents": total
194
+ ])
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ This allows swapping analytics providers, testing with a mock, and preventing typos in event names.
201
+
202
+ ### Performance Monitoring
203
+
204
+ **App startup time (iOS)**
205
+ ```swift
206
+ // Measure time from app launch to first interactive frame
207
+ // FirebasePerformance trace
208
+ let trace = Performance.startTrace(name: "app_startup")
209
+ // ... app initialization ...
210
+ trace?.stop()
211
+ ```
212
+
213
+ Instruments > App Launch template records the full cold start time with a flame graph of initialization cost. Target: under 400ms pre-main time (Swift static initializers, ObjC +load methods).
214
+
215
+ **App startup time (Android)**
216
+ ```kotlin
217
+ // Firebase Performance: automatic activity launch monitoring
218
+ // Manual trace for custom startup paths
219
+ val trace = Firebase.performance.newTrace("app_cold_start")
220
+ trace.start()
221
+ // ... initialization ...
222
+ trace.stop()
223
+ ```
224
+
225
+ Systrace and Android Studio Profiler > CPU: record app startup to identify slow initialization. Common causes: synchronous disk I/O, large dependency injection graphs, synchronous network calls.
226
+
227
+ **Network performance (Firebase Performance)**
228
+ Firebase Performance automatically monitors HTTP requests made through URLSession (iOS) and OkHttp/HttpURLConnection (Android) without code changes.
229
+
230
+ Manual HTTP tracing:
231
+ ```swift
232
+ // iOS custom network trace
233
+ let metric = HTTPMetric(url: url, httpMethod: .get)
234
+ metric?.start()
235
+ let (data, response) = try await URLSession.shared.data(from: url)
236
+ metric?.responseCode = (response as? HTTPURLResponse)?.statusCode ?? -1
237
+ metric?.stop()
238
+ ```
239
+
240
+ ```kotlin
241
+ // Android: OkHttp interceptor for custom metrics
242
+ class PerformanceInterceptor : Interceptor {
243
+ override fun intercept(chain: Interceptor.Chain): Response {
244
+ val request = chain.request()
245
+ val metric = Firebase.performance.newHttpMetric(request.url.toString(), request.method)
246
+ metric.start()
247
+ val response = chain.proceed(request)
248
+ metric.setHttpResponseCode(response.code)
249
+ metric.stop()
250
+ return response
251
+ }
252
+ }
253
+ ```
254
+
255
+ **Screen render time**
256
+ ```swift
257
+ // iOS: measure time to interactive per screen
258
+ func measureScreenLoad(screenName: String) {
259
+ let trace = Performance.startTrace(name: "screen_\(screenName)_load")
260
+ // Stop trace when first meaningful paint is complete
261
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
262
+ trace?.stop()
263
+ }
264
+ }
265
+ ```
266
+
267
+ Set performance budget alerts in Firebase Performance: alert when p75 app start > 3s, p90 network response > 2s.
268
+
269
+ ### Structured Logging
270
+
271
+ **iOS: os_log / OSLog framework**
272
+ ```swift
273
+ import OSLog
274
+
275
+ private let logger = Logger(subsystem: "com.example.myapp", category: "Checkout")
276
+
277
+ func processOrder(_ order: Order) async throws {
278
+ logger.info("Order processing started: orderId=\(order.id, privacy: .public)")
279
+ do {
280
+ let result = try await paymentService.charge(order)
281
+ logger.info("Payment succeeded: orderId=\(order.id, privacy: .public) amount=\(order.totalCents)")
282
+ } catch {
283
+ logger.error("Payment failed: orderId=\(order.id, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
284
+ throw error
285
+ }
286
+ }
287
+ ```
288
+
289
+ Privacy annotations matter:
290
+ - `privacy: .public`: logged in production — use for non-PII identifiers
291
+ - `privacy: .private`: redacted in production (shown as `<private>`) — default for user data
292
+ - `privacy: .sensitive`: always redacted — for credentials and PII
293
+
294
+ **Android: structured logging with tags**
295
+ ```kotlin
296
+ private const val TAG = "Checkout"
297
+
298
+ fun processOrder(order: Order) {
299
+ Log.i(TAG, "Order processing started orderId=${order.id}")
300
+ try {
301
+ val result = paymentService.charge(order)
302
+ Log.i(TAG, "Payment succeeded orderId=${order.id} amount=${order.totalCents}")
303
+ } catch (e: Exception) {
304
+ Log.e(TAG, "Payment failed orderId=${order.id}", e)
305
+ throw e
306
+ }
307
+ }
308
+ ```
309
+
310
+ In production, always integrate with a crash reporter (Crashlytics, Sentry) — `android.util.Log` output is not captured in release builds without a custom log handler.
311
+
312
+ **Log levels**
313
+ - DEBUG: development-only context, not emitted in production builds
314
+ - INFO: business events worth auditing (login, purchase, key feature usage)
315
+ - WARNING: recoverable errors, degraded functionality
316
+ - ERROR: failures that should be investigated (but app continues)
317
+ - FATAL/CRITICAL: unrecoverable errors preceding a crash — rarely used directly
@@ -0,0 +1,311 @@
1
+ ---
2
+ name: mobile-app-offline-patterns
3
+ description: Local storage (SQLite/Room/Core Data), sync engines, conflict resolution, and background sync for offline-capable mobile apps
4
+ topics: [mobile-app, offline, sqlite, room, core-data, sync, conflict-resolution, background-sync]
5
+ ---
6
+
7
+ Offline capability is not optional for mobile apps — cellular networks are unreliable, users enter tunnels and basements, and users expect their data to persist between sessions. The complexity of offline architecture scales with sync complexity: read-only cache is trivial; bidirectional sync with conflict resolution is one of the hardest problems in software. Define your offline model explicitly before implementing persistence.
8
+
9
+ ## Summary
10
+
11
+ Mobile offline patterns use local databases (Room+SQLite for Android, Core Data or GRDB for iOS) as the primary source of truth, with network sync as a background process. The "offline-first" pattern means the UI reads from local storage and writes to a local queue, which syncs independently. Conflict resolution strategies range from "last write wins" to CRDTs for complex cases. Background sync uses WorkManager (Android) or BGTaskScheduler (iOS) — not raw background threads.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Storage Layer Options
16
+
17
+ **Android: Room (SQLite abstraction)**
18
+
19
+ Room is the recommended local database for Android. It enforces compile-time query validation:
20
+
21
+ ```kotlin
22
+ @Entity(tableName = "users")
23
+ data class UserEntity(
24
+ @PrimaryKey val id: String,
25
+ val name: String,
26
+ val email: String,
27
+ @ColumnInfo(name = "updated_at") val updatedAt: Long,
28
+ @ColumnInfo(name = "sync_status") val syncStatus: SyncStatus = SyncStatus.SYNCED
29
+ )
30
+
31
+ enum class SyncStatus { SYNCED, PENDING_CREATE, PENDING_UPDATE, PENDING_DELETE }
32
+
33
+ @Dao
34
+ interface UserDao {
35
+ @Query("SELECT * FROM users WHERE sync_status != 'PENDING_DELETE'")
36
+ fun observeUsers(): Flow<List<UserEntity>>
37
+
38
+ @Query("SELECT * FROM users WHERE sync_status != 'SYNCED'")
39
+ suspend fun getPendingSync(): List<UserEntity>
40
+
41
+ @Upsert
42
+ suspend fun upsert(user: UserEntity)
43
+
44
+ @Query("UPDATE users SET sync_status = :status WHERE id = :id")
45
+ suspend fun updateSyncStatus(id: String, status: SyncStatus)
46
+ }
47
+
48
+ @Database(entities = [UserEntity::class], version = 1, exportSchema = true)
49
+ abstract class AppDatabase : RoomDatabase() {
50
+ abstract fun userDao(): UserDao
51
+ }
52
+ ```
53
+
54
+ Schema migrations: export schema with `exportSchema = true` (required for migration testing). Write explicit migrations:
55
+ ```kotlin
56
+ val MIGRATION_1_2 = object : Migration(1, 2) {
57
+ override fun migrate(db: SupportSQLiteDatabase) {
58
+ db.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT")
59
+ }
60
+ }
61
+ ```
62
+
63
+ **Android: DataStore for preferences**
64
+ - `Preferences DataStore`: key-value pairs for simple settings (replaces SharedPreferences)
65
+ - `Proto DataStore`: typed storage using Protocol Buffers for structured preferences
66
+ - DataStore is coroutine-native; SharedPreferences is synchronous on the main thread — never use SharedPreferences for new code
67
+
68
+ ```kotlin
69
+ val dataStore: DataStore<Preferences> = context.createDataStore(name = "settings")
70
+ val USER_TOKEN = stringPreferencesKey("user_token")
71
+
72
+ // Write
73
+ dataStore.edit { settings -> settings[USER_TOKEN] = token }
74
+
75
+ // Read
76
+ val tokenFlow: Flow<String?> = dataStore.data.map { it[USER_TOKEN] }
77
+ ```
78
+
79
+ **iOS: GRDB (SQLite wrapper)**
80
+
81
+ GRDB provides a type-safe Swift SQLite interface with Combine/async integration:
82
+
83
+ ```swift
84
+ struct User: Codable, FetchableRecord, PersistableRecord {
85
+ var id: String
86
+ var name: String
87
+ var email: String
88
+ var updatedAt: Date
89
+ var syncStatus: SyncStatus = .synced
90
+ }
91
+
92
+ // Write
93
+ try dbQueue.write { db in
94
+ try user.save(db) // INSERT OR REPLACE
95
+ }
96
+
97
+ // Observe changes
98
+ let observation = ValueObservation.tracking { db in
99
+ try User.filter(Column("syncStatus") != SyncStatus.pendingDelete).fetchAll(db)
100
+ }
101
+ observation.start(in: dbQueue) { users in
102
+ // React to database changes
103
+ }
104
+ ```
105
+
106
+ **iOS: Core Data (Apple's ORM)**
107
+
108
+ Core Data is the Apple-native persistence framework. It provides object graph management, lazy faulting, and iCloud sync (CloudKit integration):
109
+
110
+ ```swift
111
+ // NSManagedObject subclass
112
+ @objc(UserMO)
113
+ class UserMO: NSManagedObject {
114
+ @NSManaged var id: String
115
+ @NSManaged var name: String
116
+ @NSManaged var syncStatus: Int16
117
+ }
118
+
119
+ // Fetch with NSFetchedResultsController for UI reactivity
120
+ let request: NSFetchRequest<UserMO> = UserMO.fetchRequest()
121
+ request.predicate = NSPredicate(format: "syncStatus != %d", SyncStatus.pendingDelete.rawValue)
122
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \UserMO.name, ascending: true)]
123
+
124
+ let controller = NSFetchedResultsController(
125
+ fetchRequest: request,
126
+ managedObjectContext: viewContext,
127
+ sectionNameKeyPath: nil,
128
+ cacheName: nil
129
+ )
130
+ ```
131
+
132
+ Use a background `NSManagedObjectContext` for data imports — never import on the `viewContext` (main thread). Merge changes back to viewContext with `mergeChanges(fromContextDidSave:)`.
133
+
134
+ **iOS: SwiftData (iOS 17+)**
135
+
136
+ SwiftData is the modern replacement for Core Data with a Swift-native API:
137
+
138
+ ```swift
139
+ @Model
140
+ class User {
141
+ var id: String
142
+ var name: String
143
+ var email: String
144
+ var syncStatus: SyncStatus
145
+
146
+ init(id: String, name: String, email: String) {
147
+ self.id = id
148
+ self.name = name
149
+ self.email = email
150
+ self.syncStatus = .synced
151
+ }
152
+ }
153
+
154
+ // Query with SwiftUI
155
+ @Query(filter: #Predicate<User> { $0.syncStatus != .pendingDelete })
156
+ var users: [User]
157
+ ```
158
+
159
+ SwiftData targets iOS 17+ — if supporting iOS 16, use GRDB or Core Data.
160
+
161
+ ### Offline-First Architecture
162
+
163
+ **The offline-first pattern**
164
+ The UI never calls the network directly. All writes go to the local database with a `PENDING` sync status. A background sync engine reads pending items and attempts to sync:
165
+
166
+ ```
167
+ User Action → Write to Local DB (status: PENDING) → Notify UI
168
+
169
+ Background Sync Engine
170
+
171
+ POST/PATCH/DELETE to API
172
+
173
+ Success: Update status to SYNCED
174
+ Failure: Retry with exponential backoff
175
+ ```
176
+
177
+ **Repository pattern for offline-first (Android)**
178
+ ```kotlin
179
+ class UserRepository @Inject constructor(
180
+ private val userDao: UserDao,
181
+ private val userApi: UserApi,
182
+ private val syncQueue: SyncQueue
183
+ ) {
184
+ fun observeUsers(): Flow<List<User>> =
185
+ userDao.observeUsers().map { entities -> entities.map { it.toDomain() } }
186
+
187
+ suspend fun updateUser(user: User) {
188
+ // Write locally first — UI responds immediately
189
+ userDao.upsert(user.toEntity().copy(syncStatus = SyncStatus.PENDING_UPDATE))
190
+ // Enqueue sync (fires in background)
191
+ syncQueue.enqueue(SyncOperation.UpdateUser(user.id))
192
+ }
193
+ }
194
+ ```
195
+
196
+ **Sync queue implementation (Android: WorkManager)**
197
+ ```kotlin
198
+ class SyncWorker(
199
+ context: Context,
200
+ params: WorkerParameters,
201
+ private val userRepository: UserRepository,
202
+ private val userApi: UserApi
203
+ ) : CoroutineWorker(context, params) {
204
+
205
+ override suspend fun doWork(): Result {
206
+ val pending = userRepository.getPendingSync()
207
+ var hasFailure = false
208
+
209
+ for (item in pending) {
210
+ try {
211
+ when (item.syncStatus) {
212
+ SyncStatus.PENDING_CREATE -> userApi.createUser(item.toDto())
213
+ SyncStatus.PENDING_UPDATE -> userApi.updateUser(item.id, item.toDto())
214
+ SyncStatus.PENDING_DELETE -> userApi.deleteUser(item.id)
215
+ else -> continue
216
+ }
217
+ userRepository.markSynced(item.id)
218
+ } catch (e: Exception) {
219
+ hasFailure = true
220
+ // Log but continue — sync other items
221
+ }
222
+ }
223
+ return if (hasFailure) Result.retry() else Result.success()
224
+ }
225
+ }
226
+
227
+ // Schedule on connectivity restored
228
+ val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
229
+ .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
230
+ .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
231
+ .build()
232
+ WorkManager.getInstance(context).enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, syncRequest)
233
+ ```
234
+
235
+ **Sync queue implementation (iOS: BGTaskScheduler)**
236
+ ```swift
237
+ // Register in AppDelegate/App
238
+ BGTaskScheduler.shared.register(
239
+ forTaskWithIdentifier: "com.example.myapp.sync",
240
+ using: nil
241
+ ) { task in
242
+ SyncEngine.shared.performSync(task: task as! BGProcessingTask)
243
+ }
244
+
245
+ // Schedule sync
246
+ func scheduleSync() {
247
+ let request = BGProcessingTaskRequest(identifier: "com.example.myapp.sync")
248
+ request.requiresNetworkConnectivity = true
249
+ request.requiresExternalPower = false
250
+ try? BGTaskScheduler.shared.submit(request)
251
+ }
252
+ ```
253
+
254
+ ### Conflict Resolution Strategies
255
+
256
+ **Last Write Wins (LWW)**
257
+ - Simplest strategy: the record with the most recent `updatedAt` timestamp wins
258
+ - Appropriate for: user preferences, profile data where overwriting is acceptable
259
+ - Pitfall: clock skew between client and server can cause older data to win. Always use server time, not client time, as the authoritative timestamp.
260
+
261
+ ```kotlin
262
+ // Server-side merge
263
+ fun mergeUser(local: UserDto, server: UserDto): UserDto =
264
+ if (local.updatedAt > server.updatedAt) local else server
265
+ ```
266
+
267
+ **Server Wins**
268
+ - On conflict, the server version is always authoritative. Client changes are discarded.
269
+ - Appropriate for: data owned by the server (inventory, pricing), or when conflicts are rare
270
+ - Implementation: on sync, fetch the latest server version and overwrite local changes
271
+
272
+ **Client Wins**
273
+ - On conflict, the client's offline changes are always applied.
274
+ - Appropriate for: personal data the user expects to control (notes, settings, journal entries)
275
+ - Implementation: send client changes with `If-Unmodified-Since` or ETag; if conflict, apply client version and bump server version
276
+
277
+ **Three-Way Merge**
278
+ - Merge the base version, client version, and server version to produce a merged result
279
+ - Used for text fields where both sides should be preserved (collaborative editing)
280
+ - Complex to implement correctly — use a CRDT library or operational transform engine rather than writing from scratch
281
+
282
+ **CRDTs (Conflict-free Replicated Data Types)**
283
+ - Data structures that guarantee convergence without central coordination
284
+ - Appropriate for: collaborative features, distributed sync without a central server
285
+ - Common types: G-Counter (increment only), PN-Counter (increment/decrement), LWW-Register, OR-Set (add/remove set)
286
+ - Libraries: Automerge (Swift+Kotlin), Yjs (cross-platform via Wasm)
287
+
288
+ ### Network State Detection
289
+
290
+ **Android: ConnectivityManager**
291
+ ```kotlin
292
+ val connectivityManager = context.getSystemService<ConnectivityManager>()
293
+ val networkCallback = object : ConnectivityManager.NetworkCallback() {
294
+ override fun onAvailable(network: Network) { triggerSync() }
295
+ override fun onLost(network: Network) { pauseSync() }
296
+ }
297
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
298
+ ```
299
+
300
+ **iOS: NWPathMonitor**
301
+ ```swift
302
+ let monitor = NWPathMonitor()
303
+ monitor.pathUpdateHandler = { path in
304
+ if path.status == .satisfied {
305
+ Task { await SyncEngine.shared.sync() }
306
+ }
307
+ }
308
+ monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))
309
+ ```
310
+
311
+ Always debounce network transitions — connectivity can oscillate rapidly when entering/leaving coverage. Implement a minimum stable duration (e.g., 2 seconds of connectivity) before triggering sync.