Swift Sync

SQLiteNow can generate a Swift package for an Oversqlite-enabled database. The Swift app imports the generated package product, opens the local database, creates a sync client, attaches an authenticated user, runs sync, and observes sync progress from Swift async code.

Replace X.Y.Z with the latest SQLiteNow release version.

Configure A Sync Database

Use "runtime": "sync":

{
  "schemaVersion": 1,
  "databases": [
    {
      "databaseName": "AppSyncDatabase",
      "swiftPackageName": "AppSyncDatabaseSQLiteNow",
      "swiftTargetName": "AppSyncDatabaseSQLiteNow",
      "runtime": "sync"
    }
  ]
}

The released SQLiteNow SwiftPM package supplies the matching sync runtime artifact URL and checksum for its version.

With sqlDirectory omitted, the generator reads SQL from SQLiteNow/databases/AppSyncDatabase under the Swift package root. With outputDirectory omitted, it writes the generated package to SQLiteNowGenerated/AppSyncDatabaseSQLiteNow.

Mark Sync-Managed Tables

Use enableSync=true on each table that should participate in Oversqlite sync. Sync-managed tables must expose exactly one visible primary-key column, and that key must hold UUID data.

-- SQLiteNow/databases/AppSyncDatabase/schema/person.sql
-- @@{ enableSync=true }
CREATE TABLE person (
    id TEXT PRIMARY KEY NOT NULL,
    name TEXT NOT NULL
);

Then add normal query files under queries/:

-- SQLiteNow/databases/AppSyncDatabase/queries/person/insert.sql
INSERT INTO person (id, name)
VALUES (:id, :name);
-- SQLiteNow/databases/AppSyncDatabase/queries/person/selectAll.sql
SELECT *
FROM person
ORDER BY name;

The generated package includes both the typed database API and the sync client factory for the configured sync tables.

Generate And Import

Run generation from the Swift package root:

swift package plugin --allow-writing-to-package-directory sqlitenow-generate

Add the generated package to your app and import the generated product:

import AppSyncDatabaseSQLiteNow

App code should import the generated package product, not the runtime module.

Create A Sync Client

The generated database owns the local SQLite runtime. The sync client owns the HTTP bridge and exposes Swift-owned configuration and result types.

let db = AppSyncDatabase(path: databaseURL)
try await db.open()

let sync = try db.makeSyncClient(
    baseURL: URL(string: "https://sync.example.com")!,
    auth: .bearer(accessToken: { tokenStore.currentAccessToken() }),
    schema: "business",
    verboseLogs: false
)

The Swift app does not import Ktor, Kotlin StateFlow, Kotlin Result, or the internal bridge framework module.

Open, Attach, And Sync

Call open() before connected sync operations. Your app owns authentication; pass the authenticated user id returned by your server when attaching.

try await sync.open()

let source = try await sync.sourceInfo()
let signIn = try await requestSignInToken(sourceId: source.currentSourceId)

switch try await sync.attach(userId: signIn.user) {
case let .connected(_, status, _):
    print("Last seen bundle:", status.lastBundleSeqSeen)
case let .retryLater(retryAfterSeconds):
    print("Retry attach in \(retryAfterSeconds)s")
case let .unknown(raw):
    print("Unknown attach result:", raw)
}

let report = try await sync.sync()
print("Pending rows:", report.status.pending.pendingRowCount)

If sourceInfo() reports that rebuild or source recovery is required, call rebuild() before continuing normal sync.

Progress And Automatic Downloads

Progress is exposed as a Swift async sequence:

Task {
    for try await progress in sync.progress() {
        print(progress)
    }
}

Automatic downloads are default-off and return a Swift-owned cancellable handle:

let downloads = sync.startAutomaticDownloads(
    SQLiteNowAutomaticDownloadConfig(
        automaticDownloadIntervalMillis: 60_000,
        bundleChangeWatchMode: .auto
    ),
    onError: { error in
        print(error)
    }
)

downloads.cancel()

Sign Out And Close

Use syncThenDetach() when signing out so local pending work has a chance to upload before the source detaches.

let result = try await sync.syncThenDetach()
if result.success {
    sync.close()
}

Close the database when the owning app object shuts down:

try await db.close()

Server Setup

SQLiteNow sync needs a compatible Oversqlite server. See the Sync server setup guide for server requirements and deployment guidance.

For broader sync lifecycle concepts, see the sync core concepts guide.