
Enables type-safe SQLite database access with auto-generated code from SQL files, comment-based annotations for code control, and no need for IDE plugins, focusing on SQLite-specific optimizations.
If SQLiteNow saves you time, please consider starring ⭐ the repository - it helps more KMP, Flutter/Dart, and Swift developers find it.
SQLiteNow is SQL-first tooling for type-safe SQLite access in Kotlin Multiplatform and Flutter/Dart apps, plus native Swift apps through a local generated-package workflow. Write schema, migration, and query files in SQL, then generate platform-native code with typed parameters, typed results, migrations, transactions, and reactive invalidation.
Full documentation is available at https://mobiletoly.github.io/sqlitenow-kmp/.
SQLiteNow keeps SQL as the source of truth while still giving application code a typed API. It is focused exclusively on SQLite instead of abstracting over multiple database engines, which leaves room for SQLite-specific behavior, annotations, migrations, and sync-aware generated code.
The main goals are:
.sql files for schema, migrations, and
queries.-- @@{ queryResult=... }, dynamicField, and mapTo to shape generated
models.SQLiteNow can shape generated result models directly from SQL annotations:
-- @@{ queryResult=PersonWithAddresses }
SELECT p.id,
p.first_name,
p.last_name,
p.email,
p.created_at,
a.address_type,
a.postal_code,
a.country,
a.street,
a.city,
a.state
/* @@{ dynamicField=addresses,
mappingType=collection,
propertyType=List<Address>,
sourceTable=a,
collectionKey=address_id } */
FROM Person p
LEFT JOIN PersonAddress a ON p.id = a.person_id
ORDER BY p.id, a.address_type
LIMIT :limit OFFSET :offsetThis generates a PersonWithAddresses result model with
addresses: List<Address> plus typed query parameters for limit and offset.
Adapters can convert between SQLite values and domain types, and mapTo can
shape data further when the generated model should match application-layer
types.
Full examples are available in /sample-kmp for KMP,
/dart/examples for Dart/Flutter, and
/swift/samples/core for native Swift.
| Platform | Use this when | Status | Start here |
|---|---|---|---|
| Kotlin Multiplatform | You are building shared Kotlin apps for Android, iOS, desktop, JS, or Wasm. | Released Gradle plugin and KMP runtime libraries | KMP guide |
| Flutter/Dart | You are building Flutter apps or pure Dart packages. | Released Dart runtime and CLI packages | Flutter/Dart guide |
| Native Swift local package | You want Swift/Xcode code to consume a generated local SwiftPM package. | Local generated package workflow; release artifacts still in progress | Swift guide |
Supported target details:
macosArm64, linuxX64, linuxArm64,
JVM desktop/server, JavaScript browser, and Kotlin/Wasm browser.package:sqlite3,
plus pure Dart packages through the Dart runtime.Native Swift support currently uses local generated Swift packages on Apple
platforms. Core Swift apps can keep SQL in a Swift/Xcode repository, run the
sqlitenow-generate SwiftPM command plugin, and import the generated package
product. The generator still requires Java 17 or newer, and published runtime
binary artifacts, a Swift-native generator/runtime, and a public SWIFT
compiler backend are not released yet. Native Swift runtime artifacts are
arm64-only: macosArm64, iosArm64, and iosSimulatorArm64.
Each platform has a complete setup guide. The short version is the same across all runtimes: keep SQL in your app repository, configure one or more databases, run the generator, and use the generated API from application code.
Full setup: KMP getting started
KMP apps use the dev.goquick.sqlitenow Gradle plugin and the
dev.goquick.sqlitenow:core runtime dependency. Configure one or more
databases in Gradle:
sqliteNow {
databases {
create("SampleDatabase") {
packageName.set("com.example.app.db")
}
}
}Place SQL files under the matching database directory:
src/commonMain/sql/SampleDatabase/
schema/
queries/
init/
migration/
Generate the database API with the task created from the database name:
./gradlew :composeApp:generateSampleDatabaseReplace :composeApp with your KMP module path and SampleDatabase with your
database name. Generated Kotlin is written under build/generated/sqlitenow/code
and is wired into commonMain by the Gradle plugin. Treat that directory as
generated output: edit SQL/configuration and rerun the task instead of
hand-editing the generated files.
For a complete app walkthrough, follow the Mood Tracker tutorial series and browse the sample project.
Full setup: Flutter/Dart getting started
Flutter and Dart apps use sqlitenow_runtime plus sqlitenow_cli:
dependencies:
sqlitenow_runtime: ^X.Y.Z
dev_dependencies:
sqlitenow_cli: ^X.Y.ZConfigure generation in sqlitenow.yaml:
databases:
AppDatabase:
input: lib/db/sql/AppDatabase
output: lib/db/generated
package: app.db
runtime: dartGenerate Dart code:
flutter pub run sqlitenow_cli generateFor pure Dart packages, use:
dart run sqlitenow_cli generateGenerated Dart is written to the configured output directory, such as
lib/db/generated/app_database.dart. Edit SQL/configuration and regenerate
instead of hand-editing generated Dart.
Full setup: Swift getting started
Swift apps add the released SQLiteNow SwiftPM package to the Swift package that
owns the SQL files, then configure generation with SQLiteNow.json:
{
"schemaVersion": 1,
"databases": [
{
"databaseName": "AppDatabase",
"swiftPackageName": "AppDatabaseSQLiteNow",
"swiftTargetName": "AppDatabaseSQLiteNow",
"runtime": "core"
}
]
}By default, SQL is read from:
SQLiteNow/databases/AppDatabase/
Generate the local Swift package from the Swift package root:
swift package plugin --allow-writing-to-package-directory sqlitenow-generateBy default, the generated package is written to:
SQLiteNowGenerated/AppDatabaseSQLiteNow
Add that generated directory as a local SwiftPM dependency, link the generated product to the app target, and import it from Swift:
import AppDatabaseSQLiteNowTreat SQLiteNowGenerated/ as generated output. Change SQL or
SQLiteNow.json, then rerun sqlitenow-generate.
SQLiteNow includes Oversqlite, an optional synchronization system for multi-device applications with conflict resolution and offline-first behavior. Sync setup requires three explicit pieces on each client:
Oversqlite canonical bytes use RFC 8785 JCS plus the
jcs_uniform_numeric_strings_v1 contract. SQLite INTEGER and finite REAL business values upload
as canonical JSON strings; exact decimals stay SQLite TEXT and JSON strings. Floating strings use
the shortest finite binary64 spelling and normalize negative zero to "0". SQLite Boolean
affinity uses strict "0"/"1" ingress strings, while authoritative server output uses JSON
Booleans. There is no per-column numeric configuration: the generated enableSync table list is
the complete client metadata surface. SQLiteNow library-core and generated output with
oversqlite = false are unchanged.
Only fresh Oversqlite databases using this canonicalization and numeric contract are supported. There is no in-place durable-state migration, canonicalization or hash fallback, or mixed-version mode. When aligning an existing deployment with this contract, release compatible server and client versions together and recreate the client databases, discarding outboxes, checkpoints, retry state, and offline work.
The sync system automatically handles:
If you have a table to sync, annotate it with enableSync=true:
-- Enable sync for this table
-- @@{ enableSync=true }
CREATE TABLE person (
id TEXT PRIMARY KEY NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);Sync-enabled tables must use exactly one local PRIMARY KEY column of type TEXT or BLOB.
INTEGER/BIGINT sync keys are rejected, and local sync-enabled tables must not model the
reserved server scope column _sync_scope_id.
Add the KMP runtime artifacts and enable Oversqlite in the Gradle DSL:
commonMain.dependencies {
implementation("dev.goquick.sqlitenow:core:<version>")
implementation("dev.goquick.sqlitenow:oversqlite:<version>")
}
sqliteNow {
databases {
create("AppDatabase") {
packageName = "com.example.app.db"
oversqlite = true
}
}
}Then use the generated sync client in your application:
// Create authenticated HTTP client with JWT token refresh and base URL
val httpClient = HttpClient {
install(Auth) {
bearer {
loadTokens { /* load saved token */ }
refreshTokens { /* refresh when expired */ }
}
}
defaultRequest {
url("https://api.myapp.com")
}
}
// Create sync client
val syncClient = db.newOversqliteClient(
schema = "myapp",
httpClient = httpClient,
resolver = ServerWinsResolver
)
// Open local runtime and attach the authenticated account.
syncClient.open().getOrThrow()
syncClient.attach(userId = "user123").getOrThrow()
// Perform full sync (upload local changes, download remote changes)
syncClient.sync().getOrThrow()
// Optional: start default-off automatic downloads.
// Bundle-change watch is only a wake-up hint; pullToStable() remains authoritative.
val automaticDownloads = coroutineScope.launch {
syncClient.runAutomaticDownloads(
db.buildOversqliteAutomaticDownloadConfig(
bundleChangeWatchMode = BundleChangeWatchMode.AUTO,
),
)
}
automaticDownloads.cancelAndJoin()KMP sync example: /samplesync-kmp.
Add the Dart runtime packages and enable Oversqlite in sqlitenow.yaml.
Replace X.Y.Z with the latest SQLiteNow release version.
dependencies:
sqlitenow_runtime: ^X.Y.Z
sqlitenow_oversqlite: ^X.Y.Z
dev_dependencies:
sqlitenow_cli: ^X.Y.Zdatabases:
AppDatabase:
input: lib/db/sql/AppDatabase
output: lib/db/generated
package: app.db
runtime: dart
oversqlite: trueThen use the generated sync client in your application:
final httpClient = IoOversqliteHttpClient(
baseUri: Uri.parse('https://api.myapp.com'),
defaultHeaders: {
HttpHeaders.authorizationHeader: 'Bearer $token',
},
);
final syncClient = db.newOversqliteClient(
schema: 'myapp',
httpClient: httpClient,
);
await syncClient.open();
await syncClient.attach('user123');
await syncClient.sync();
// Optional: start default-off automatic downloads.
// Bundle-change watch is only a wake-up hint; pullToStable() remains authoritative.
final automaticDownloads = syncClient.startAutomaticDownloads();
await automaticDownloads.stop();Dart sync package and realserver coverage:
/dart/packages/sqlitenow_oversqlite.
Client-side framework components:
Server-side component:
SQLiteNow Generator and SQLiteNow Library can be used without Oversqlite. Oversqlite can also synchronize a SQLite database with PostgreSQL without using SQLiteNow code generation.
Full documentation is available at https://mobiletoly.github.io/sqlitenow-kmp/.
If SQLiteNow saves you time, please consider starring ⭐ the repository - it helps more KMP, Flutter/Dart, and Swift developers find it.
SQLiteNow is SQL-first tooling for type-safe SQLite access in Kotlin Multiplatform and Flutter/Dart apps, plus native Swift apps through a local generated-package workflow. Write schema, migration, and query files in SQL, then generate platform-native code with typed parameters, typed results, migrations, transactions, and reactive invalidation.
Full documentation is available at https://mobiletoly.github.io/sqlitenow-kmp/.
SQLiteNow keeps SQL as the source of truth while still giving application code a typed API. It is focused exclusively on SQLite instead of abstracting over multiple database engines, which leaves room for SQLite-specific behavior, annotations, migrations, and sync-aware generated code.
The main goals are:
.sql files for schema, migrations, and
queries.-- @@{ queryResult=... }, dynamicField, and mapTo to shape generated
models.SQLiteNow can shape generated result models directly from SQL annotations:
-- @@{ queryResult=PersonWithAddresses }
SELECT p.id,
p.first_name,
p.last_name,
p.email,
p.created_at,
a.address_type,
a.postal_code,
a.country,
a.street,
a.city,
a.state
/* @@{ dynamicField=addresses,
mappingType=collection,
propertyType=List<Address>,
sourceTable=a,
collectionKey=address_id } */
FROM Person p
LEFT JOIN PersonAddress a ON p.id = a.person_id
ORDER BY p.id, a.address_type
LIMIT :limit OFFSET :offsetThis generates a PersonWithAddresses result model with
addresses: List<Address> plus typed query parameters for limit and offset.
Adapters can convert between SQLite values and domain types, and mapTo can
shape data further when the generated model should match application-layer
types.
Full examples are available in /sample-kmp for KMP,
/dart/examples for Dart/Flutter, and
/swift/samples/core for native Swift.
| Platform | Use this when | Status | Start here |
|---|---|---|---|
| Kotlin Multiplatform | You are building shared Kotlin apps for Android, iOS, desktop, JS, or Wasm. | Released Gradle plugin and KMP runtime libraries | KMP guide |
| Flutter/Dart | You are building Flutter apps or pure Dart packages. | Released Dart runtime and CLI packages | Flutter/Dart guide |
| Native Swift local package | You want Swift/Xcode code to consume a generated local SwiftPM package. | Local generated package workflow; release artifacts still in progress | Swift guide |
Supported target details:
macosArm64, linuxX64, linuxArm64,
JVM desktop/server, JavaScript browser, and Kotlin/Wasm browser.package:sqlite3,
plus pure Dart packages through the Dart runtime.Native Swift support currently uses local generated Swift packages on Apple
platforms. Core Swift apps can keep SQL in a Swift/Xcode repository, run the
sqlitenow-generate SwiftPM command plugin, and import the generated package
product. The generator still requires Java 17 or newer, and published runtime
binary artifacts, a Swift-native generator/runtime, and a public SWIFT
compiler backend are not released yet. Native Swift runtime artifacts are
arm64-only: macosArm64, iosArm64, and iosSimulatorArm64.
Each platform has a complete setup guide. The short version is the same across all runtimes: keep SQL in your app repository, configure one or more databases, run the generator, and use the generated API from application code.
Full setup: KMP getting started
KMP apps use the dev.goquick.sqlitenow Gradle plugin and the
dev.goquick.sqlitenow:core runtime dependency. Configure one or more
databases in Gradle:
sqliteNow {
databases {
create("SampleDatabase") {
packageName.set("com.example.app.db")
}
}
}Place SQL files under the matching database directory:
src/commonMain/sql/SampleDatabase/
schema/
queries/
init/
migration/
Generate the database API with the task created from the database name:
./gradlew :composeApp:generateSampleDatabaseReplace :composeApp with your KMP module path and SampleDatabase with your
database name. Generated Kotlin is written under build/generated/sqlitenow/code
and is wired into commonMain by the Gradle plugin. Treat that directory as
generated output: edit SQL/configuration and rerun the task instead of
hand-editing the generated files.
For a complete app walkthrough, follow the Mood Tracker tutorial series and browse the sample project.
Full setup: Flutter/Dart getting started
Flutter and Dart apps use sqlitenow_runtime plus sqlitenow_cli:
dependencies:
sqlitenow_runtime: ^X.Y.Z
dev_dependencies:
sqlitenow_cli: ^X.Y.ZConfigure generation in sqlitenow.yaml:
databases:
AppDatabase:
input: lib/db/sql/AppDatabase
output: lib/db/generated
package: app.db
runtime: dartGenerate Dart code:
flutter pub run sqlitenow_cli generateFor pure Dart packages, use:
dart run sqlitenow_cli generateGenerated Dart is written to the configured output directory, such as
lib/db/generated/app_database.dart. Edit SQL/configuration and regenerate
instead of hand-editing generated Dart.
Full setup: Swift getting started
Swift apps add the released SQLiteNow SwiftPM package to the Swift package that
owns the SQL files, then configure generation with SQLiteNow.json:
{
"schemaVersion": 1,
"databases": [
{
"databaseName": "AppDatabase",
"swiftPackageName": "AppDatabaseSQLiteNow",
"swiftTargetName": "AppDatabaseSQLiteNow",
"runtime": "core"
}
]
}By default, SQL is read from:
SQLiteNow/databases/AppDatabase/
Generate the local Swift package from the Swift package root:
swift package plugin --allow-writing-to-package-directory sqlitenow-generateBy default, the generated package is written to:
SQLiteNowGenerated/AppDatabaseSQLiteNow
Add that generated directory as a local SwiftPM dependency, link the generated product to the app target, and import it from Swift:
import AppDatabaseSQLiteNowTreat SQLiteNowGenerated/ as generated output. Change SQL or
SQLiteNow.json, then rerun sqlitenow-generate.
SQLiteNow includes Oversqlite, an optional synchronization system for multi-device applications with conflict resolution and offline-first behavior. Sync setup requires three explicit pieces on each client:
Oversqlite canonical bytes use RFC 8785 JCS plus the
jcs_uniform_numeric_strings_v1 contract. SQLite INTEGER and finite REAL business values upload
as canonical JSON strings; exact decimals stay SQLite TEXT and JSON strings. Floating strings use
the shortest finite binary64 spelling and normalize negative zero to "0". SQLite Boolean
affinity uses strict "0"/"1" ingress strings, while authoritative server output uses JSON
Booleans. There is no per-column numeric configuration: the generated enableSync table list is
the complete client metadata surface. SQLiteNow library-core and generated output with
oversqlite = false are unchanged.
Only fresh Oversqlite databases using this canonicalization and numeric contract are supported. There is no in-place durable-state migration, canonicalization or hash fallback, or mixed-version mode. When aligning an existing deployment with this contract, release compatible server and client versions together and recreate the client databases, discarding outboxes, checkpoints, retry state, and offline work.
The sync system automatically handles:
If you have a table to sync, annotate it with enableSync=true:
-- Enable sync for this table
-- @@{ enableSync=true }
CREATE TABLE person (
id TEXT PRIMARY KEY NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);Sync-enabled tables must use exactly one local PRIMARY KEY column of type TEXT or BLOB.
INTEGER/BIGINT sync keys are rejected, and local sync-enabled tables must not model the
reserved server scope column _sync_scope_id.
Add the KMP runtime artifacts and enable Oversqlite in the Gradle DSL:
commonMain.dependencies {
implementation("dev.goquick.sqlitenow:core:<version>")
implementation("dev.goquick.sqlitenow:oversqlite:<version>")
}
sqliteNow {
databases {
create("AppDatabase") {
packageName = "com.example.app.db"
oversqlite = true
}
}
}Then use the generated sync client in your application:
// Create authenticated HTTP client with JWT token refresh and base URL
val httpClient = HttpClient {
install(Auth) {
bearer {
loadTokens { /* load saved token */ }
refreshTokens { /* refresh when expired */ }
}
}
defaultRequest {
url("https://api.myapp.com")
}
}
// Create sync client
val syncClient = db.newOversqliteClient(
schema = "myapp",
httpClient = httpClient,
resolver = ServerWinsResolver
)
// Open local runtime and attach the authenticated account.
syncClient.open().getOrThrow()
syncClient.attach(userId = "user123").getOrThrow()
// Perform full sync (upload local changes, download remote changes)
syncClient.sync().getOrThrow()
// Optional: start default-off automatic downloads.
// Bundle-change watch is only a wake-up hint; pullToStable() remains authoritative.
val automaticDownloads = coroutineScope.launch {
syncClient.runAutomaticDownloads(
db.buildOversqliteAutomaticDownloadConfig(
bundleChangeWatchMode = BundleChangeWatchMode.AUTO,
),
)
}
automaticDownloads.cancelAndJoin()KMP sync example: /samplesync-kmp.
Add the Dart runtime packages and enable Oversqlite in sqlitenow.yaml.
Replace X.Y.Z with the latest SQLiteNow release version.
dependencies:
sqlitenow_runtime: ^X.Y.Z
sqlitenow_oversqlite: ^X.Y.Z
dev_dependencies:
sqlitenow_cli: ^X.Y.Zdatabases:
AppDatabase:
input: lib/db/sql/AppDatabase
output: lib/db/generated
package: app.db
runtime: dart
oversqlite: trueThen use the generated sync client in your application:
final httpClient = IoOversqliteHttpClient(
baseUri: Uri.parse('https://api.myapp.com'),
defaultHeaders: {
HttpHeaders.authorizationHeader: 'Bearer $token',
},
);
final syncClient = db.newOversqliteClient(
schema: 'myapp',
httpClient: httpClient,
);
await syncClient.open();
await syncClient.attach('user123');
await syncClient.sync();
// Optional: start default-off automatic downloads.
// Bundle-change watch is only a wake-up hint; pullToStable() remains authoritative.
final automaticDownloads = syncClient.startAutomaticDownloads();
await automaticDownloads.stop();Dart sync package and realserver coverage:
/dart/packages/sqlitenow_oversqlite.
Client-side framework components:
Server-side component:
SQLiteNow Generator and SQLiteNow Library can be used without Oversqlite. Oversqlite can also synchronize a SQLite database with PostgreSQL without using SQLiteNow code generation.
Full documentation is available at https://mobiletoly.github.io/sqlitenow-kmp/.