← Back to Blog

Safe SQLite Concurrency in macOS using Swift Actors and GRDB

Building a native macOS utility app comes with a strict, unspoken rule: Never drop a frame. Users expect utility apps to feel like an extension of the operating system. If your app stutters while they are typing, they will uninstall it immediately.

This creates a massive architectural challenge for data-intensive applications. In L2Cache, our developer clipboard manager, the app runs a background loop that polls the NSPasteboard every 0.5 seconds. If a new clipboard item is found, the app needs to process it, save it to a local SQLite database, and update an FTS5 (Full-Texto Search) index.

If any of those database operations accidentally touch the main UI thread, the entire macOS interface will micro-stutter.

The Magic of GRDB and Swift Actors

To solve this, we rely on GRDB.swift, an incredibly robust SQLite toolkit for Swift, paired with modern Swift Concurrency (specifically, Actor isolation).

In the pre-async/await era, handling SQLite concurrency usually meant juggling complex `DispatchQueue` callbacks and hoping you didn't accidentally capture the main thread. Today, Swift provides a native construct to guarantee thread safety: Actors.

By declaring our entire ClipStore class as a Global Actor, we ensure that calls from the Main Actor are suspended safely rather than blocking synchronously. This runs heavy SQLite work entirely off the main thread.

@globalActor
public actor ClipStoreActor {
    public static let shared = ClipStoreActor()
}

@ClipStoreActor
class ClipStore {
    private let dbQueue: DatabaseQueue
    
    init(databaseURL: URL) throws {
        self.dbQueue = try DatabaseQueue(path: databaseURL.path)
    }
    
    func save(clip: ClipItem) async throws {
        try await dbQueue.write { db in
            try clip.insert(db)
        }
    }
}

Seamless UI Updates with Asynchronous Streams

Writing to the database in the background is only half the battle. When a new clip is saved, the UI (which must run on the Main Actor) needs to be updated instantly.

GRDB makes this incredibly easy using ValueObservation. We can observe the database for changes and yield those changes back to the UI using an AsyncStream, crossing the actor boundary safely.

func observeAll() -> AsyncStream<[ClipItem]> {
    AsyncStream { continuation in
        let observation = ValueObservation.tracking { db in
            try ClipItem.fetchAll(db, sql: "SELECT * FROM clips ORDER BY created_at DESC")
        }
        
        let observer = observation.start(in: dbQueue,
            onError: { error in
                print("Observation error: \(error)")
            },
            onChange: { clips in
                continuation.yield(clips)
            })
            
        continuation.onTermination = { @Sendable _ in
            observer.cancel()
        }
    }
}

The Result: A Zero-Jank Experience

By combining Swift's @globalActor with GRDB's serialized database queue and ValueObservations, L2Cache achieves a state where:

If you are building a modern, data-intensive macOS or iOS app, leaning fully into Swift Concurrency and GRDB is the best architectural decision you can make.

Try a truly native macOS experience

L2Cache is a completely native, privacy-first clipboard manager built specifically for developers.

Baixar Gratuito Early Access