GRDB Across App Groups
Discipline for sharing one SQLite database between your main app and its widgets, extensions, or Live Activities on iOS. GRDB's own documentation calls this "extremely difficult on iOS" and "almost impossible to test", so the skill turns it into an explicit checklist.
When to Use
Use this skill when you're seeing any of these:
- "My widget shows stale data from the app's database"
- "My Live Activity can't open the database while the device is locked"
- "App keeps getting killed with
0xDEAD10CCin crash logs" - "App works in dev but crashes after TestFlight upload"
- "Two processes hit
SQLITE_BUSYand never recover" - "SQLite error 10 (
SQLITE_IOERR) only on locked devices" - "Why does my widget see different data than the app?"
- You're starting a new widget or extension that must read the app's GRDB database safely
This skill covers a different symptom class from grdb-performance, which fires on "query slow"; this one fires on "process boundary violated".
Example Prompts
Questions you can ask your assistant that will draw from this skill:
- "My widget needs to read the same database as the app. How do I set it up safely?"
- "I shipped to TestFlight and now I'm getting
0xDEAD10CCcrashes. What's causing them?" - "My Live Activity can't read the database after the device locks. Why?"
- "How do I notify my widget when the app writes to the database?"
- "Should I use
DatabaseQueueorDatabasePoolwhen my app and widget share a database?" - "What Data Protection class should my shared SQLite file use?"
What This Skill Provides
Prefer Not to Share
- The snapshot-file pattern: the app writes a derived JSON or read-only SQLite file for the widget
- WidgetCenter timelines computed by the app, so the widget never opens the database
- Shared
UserDefaultsfor small datasets - Darwin notifications that ask the app to refresh instead of opening the database from the widget
Mandatory Setup
- The App Groups entitlement on every target, all listing the same group identifier
- Resolving the container URL
- A dedicated subdirectory for the
.db,-wal, and-shmfiles - Running the same versioned migration in every process before it writes, so an FTS5 index can't drift
Mandatory PRAGMAs and Configuration
DatabasePool, which enables WAL automatically and allows concurrent reads- Persistent WAL through the
SQLITE_FCNTL_PERSIST_WALfile control Configuration.busyMode = .timeout(5)locking_mode = NORMAL, neverEXCLUSIVEPRAGMA optimizein the writer process, since read-only widgets can't refresh the planner's statistics
Data Protection
- Why an explicit
.completebreaks widgets after auto-lock .completeUntilFirstUserAuthenticationfor shared databases- Applying one class to all three files (
.db,-wal,-shm), and again after any migration that recreates them - Why iOS 17's
.completeWhenUserInactiveblocks widget reads during idle periods
Suspension Defense (0xDEAD10CC)
- How the iOS suspension watchdog terminates an app that still holds SQLite locks
Configuration.observesSuspensionNotifications = true- Posting
Database.suspendNotificationandresumeNotificationfrom the app lifecycle - Suspend on entering the background, not on resigning active –
.inactiveis transient - SwiftUI
scenePhasewiring, with.inactiveas a no-op - Catching
SQLITE_ABORT(4) andSQLITE_INTERRUPT(9) on the write path
File Coordination on Open
NSFileCoordinatorto stop writer and reader races at open time- Read-only opens with
readingItemAt:and.withoutChanges - Preventing migration races on the first multi-process launch
Cross-Process Change Notification
DatabaseRegionObservationto detect every commit- Darwin notifications through
CFNotificationCenterGetDarwinNotifyCenter - Why
deliverImmediatelyis ignored on the Darwin center
SQLITE_BUSY Retry
- Why
SQLITE_BUSYis expected, not fatal, when several processes share a database - Exponential backoff (50 ms, 200 ms, 500 ms) wrapped around the whole
writecall - When to show the user a "sync busy" state
Key Pattern
Persistent WAL setup
import GRDB
import SQLite3
var config = Configuration()
config.busyMode = .timeout(5)
config.observesSuspensionNotifications = true
config.prepareDatabase { db in
// Keep -wal and -shm files on disk so read-only processes can attach
var flag: CInt = 1
let code = withUnsafeMutablePointer(to: &flag) { ptr -> CInt in
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, ptr)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: ResultCode(rawValue: code))
}
try db.execute(sql: "PRAGMA locking_mode = NORMAL")
}Suspension defense wiring (SwiftUI)
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup { ContentView() }
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
NotificationCenter.default.post(
name: Database.resumeNotification, object: nil)
case .background:
NotificationCenter.default.post(
name: Database.suspendNotification, object: nil)
case .inactive:
break // Transient — do NOT suspend here
@unknown default:
break
}
}
}Cross-process change broadcast
let regionObservation = DatabaseRegionObservation(tracking: .fullDatabase)
let cancellable = regionObservation.start(in: dbPool) { error in
// log
} onChange: { db in
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName("com.example.app.db.changed" as CFString),
nil, nil, false // deliverImmediately is ignored on Darwin center
)
}Documentation Scope
This page documents the grdb-app-groups skill (in the axiom-data suite). For an automated scan, use the grdb-performance-auditor agent, which flags journal-mode mismatches and missing suspension defense in app-group databases.
Related
- grdb – GRDB primer for setup and queries
- grdb-performance – performance discipline, including the
PRAGMA optimizesetup the writer process runs for shared databases - database-migration – the versioned migrations every process runs before it writes
- sqlite-fts-ref – FTS5 sync triggers, and why a process that writes before the migration leaves the index stale
- icloud-drive-ref – iCloud Drive file sharing, a different model from a shared database
- storage – deciding where data lives before you decide to share it
Resources
GRDB docs: github.com/groue/GRDB.swift, Documentation/DatabaseSharing.md
Docs: /xcode/configuring-app-groups, /foundation/fileprotectiontype, /foundation/nsfilecoordinator