How to Use Kysely with Capacitor and SQLite¶
Working with raw SQL in a Capacitor app gets messy fast — queries are just strings, results are untyped, and refactoring a column name means hunting through your entire codebase. Kysely solves this with a type-safe query builder that catches errors at compile time while keeping you close to SQL. In this guide, you'll learn how to set up Kysely with the SQLite plugin using the new @capawesome/capacitor-sqlite-kysely dialect.
What is Kysely?¶
Kysely (pronounced "Key-seh-lee") is a type-safe TypeScript SQL query builder. It's not a traditional ORM that hides SQL behind abstract methods — instead, it gives you a fluent API that maps directly to SQL, with full type inference at every step.
Here's what makes it a good fit for Capacitor apps:
- Type safety — Queries are validated against your database types at compile time. If you reference a column that doesn't exist, TypeScript catches it before the code runs.
- SQL-first — The API mirrors SQL syntax closely. If you know
SELECT,WHERE,JOIN, andINSERT, you already know how to use Kysely. - Dialect system — Kysely uses a pluggable dialect architecture, making it straightforward to integrate with different database backends — including Capacitor SQLite.
- Built-in migrations — Kysely includes a
Migratorclass that lets you define and run migrations in TypeScript. No external tooling required. - Lightweight — Kysely has no runtime dependencies and a small footprint, which keeps your app bundle lean.
Prerequisites¶
Before you begin, make sure you have a Capacitor project with the SQLite plugin installed. To install the plugin, please refer to the Installation section in the plugin documentation.
Installation¶
Install the Kysely dialect along with Kysely itself:
Setting Up the Database¶
To get started, open a database using the SQLite plugin and create a Kysely instance with the CapacitorSqliteDialect:
import { Sqlite } from '@capawesome-team/capacitor-sqlite';
import { Kysely } from 'kysely';
import { CapacitorSqliteDialect } from '@capawesome/capacitor-sqlite-kysely';
const { databaseId } = await Sqlite.open({ path: 'my.db' });
const db = new Kysely<Database>({
dialect: new CapacitorSqliteDialect(Sqlite, { databaseId }),
});
The CapacitorSqliteDialect takes two arguments: the Sqlite plugin instance and a configuration object with the databaseId returned by open(...). The Database generic parameter is a TypeScript interface that describes your tables — we'll define that next.
Defining Your Database Types¶
Kysely uses TypeScript interfaces to describe your database schema. This is what powers its type inference — every query you write is checked against these types at compile time.
Create a types file for your database:
import { Generated } from 'kysely';
interface Database {
users: UsersTable;
posts: PostsTable;
}
interface UsersTable {
id: Generated<number>;
name: string;
email: string;
}
interface PostsTable {
id: Generated<number>;
title: string;
content: string | null;
author_id: number;
}
A few things to note here:
- Each key in the
Databaseinterface corresponds to a table name in your database. Generated<number>marks a column as auto-generated (e.g. an auto-incrementing primary key). Kysely will make this column optional inINSERTstatements but required inSELECTresults.- Nullable columns use a union type with
null(e.g.string | null). - These types don't create tables — they only describe the shape of your data for TypeScript's type checker.
Running Queries¶
With the database types in place, Kysely gives you a fluent, chainable API for building SQL queries. Every query is fully typed based on your Database interface.
Insert¶
Select¶
// Select all users
const allUsers = await db.selectFrom('users').selectAll().execute();
// Select with a filter
const user = await db
.selectFrom('users')
.selectAll()
.where('email', '=', 'alice@example.com')
.executeTakeFirst();
The executeTakeFirst() method returns a single result or undefined, which is useful when you expect at most one row.
Update¶
Delete¶
Every query is validated at compile time. If you mistype a column name or pass the wrong type, TypeScript will flag it immediately.
Transactions¶
For operations that need to succeed or fail atomically, use transactions. Kysely manages BEGIN, COMMIT, and ROLLBACK automatically:
await db.transaction().execute(async (trx) => {
await trx
.insertInto('users')
.values({ name: 'Alice', email: 'alice@example.com' })
.execute();
await trx
.insertInto('posts')
.values({ title: 'Hello World', content: '...', author_id: 1 })
.execute();
});
If any statement inside the callback throws an error, the entire transaction is rolled back. This is essential for maintaining data consistency when inserting related records across multiple tables.
Migrations¶
Kysely includes a built-in Migrator class for managing database schema changes. Since the CapacitorSqliteDialect implements Kysely's standard Dialect interface, migrations work out of the box — no extra tooling or bundler plugins needed.
Define your migrations as a MigrationProvider:
import { Kysely, Migrator, MigrationProvider } from 'kysely';
const migrationProvider: MigrationProvider = {
async getMigrations() {
return {
'001_create_users': {
async up(db: Kysely<any>) {
await db.schema
.createTable('users')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', (col) => col.notNull().unique())
.execute();
},
},
'002_create_posts': {
async up(db: Kysely<any>) {
await db.schema
.createTable('posts')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('title', 'text', (col) => col.notNull())
.addColumn('content', 'text')
.addColumn('author_id', 'integer', (col) => col.notNull().references('users.id'))
.execute();
},
},
};
},
};
Then apply the migrations when your app starts:
const migrator = new Migrator({ db, provider: migrationProvider });
const { error, results } = await migrator.migrateToLatest();
if (error) {
console.error('Migration failed:', error);
}
Migrations are written in TypeScript using Kysely's schema builder, which means they benefit from the same type safety and autocompletion as your queries. The Migrator tracks applied migrations automatically, so calling migrateToLatest() multiple times is safe — only pending migrations are executed.
Stay Updated¶
Want to stay up to date with the latest features and guides? Subscribe to the Capawesome newsletter.
Subscribe to the Capawesome Newsletter
Conclusion¶
With the @capawesome/capacitor-sqlite-kysely dialect, you can use Kysely's type-safe query builder and built-in migration system directly in your Capacitor apps. The setup is minimal: define your database types as TypeScript interfaces, create a dialect instance, and start writing queries that are checked at compile time — all while staying close to SQL.
If you're looking for an alternative approach with schema-as-code and relational queries, check out our guide on How to Use Drizzle ORM with Capacitor and SQLite. For the full API reference and source code, visit the adapter on GitHub. If you have questions or feedback, join the Capawesome Discord server to connect with the community. And subscribe to the Capawesome newsletter to stay updated on the latest news.