---
title: How to Use Kysely with Capacitor and SQLite
description: Learn how to use Kysely with Capacitor and SQLite to build type-safe database layers with a fluent query builder in your mobile apps.
date:
  created: 2026-02-26
  updated: 2026-07-17
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor SQLite: sdks/capacitor/sqlite.md
faq: true
---

# 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 [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md) using the new `@capawesome/capacitor-sqlite-kysely` dialect.

For the **Capacitor SQLite plugin** API, see the [plugin documentation](../../sdks/capacitor/sqlite.md#api).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

## What is Kysely?

[Kysely](https://kysely.dev/){:target="_blank"} (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`, and `INSERT`, 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 `Migrator` class 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 [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md) installed. To install the plugin, please refer to the [Installation](../../sdks/capacitor/sqlite.md#installation) section in the plugin documentation. If the plugin is new to you, start with [How to Use SQLite in a Capacitor App](./how-to-use-sqlite-in-a-capacitor-app.md) to see the underlying database API the Kysely dialect wraps.

## Installation

Install the Kysely dialect along with Kysely itself:

```bash
npm install @capawesome/capacitor-sqlite-kysely kysely
```

## Setting Up the Database

To get started, open a database using the [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md) and create a Kysely instance with the `CapacitorSqliteDialect`:

```typescript
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(...)`](../../sdks/capacitor/sqlite.md#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:

```typescript
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 `Database` interface 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 in `INSERT` statements but required in `SELECT` results.
- 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

```typescript
await db
  .insertInto('users')
  .values({ name: 'Alice', email: 'alice@example.com' })
  .execute();
```

### Select

```typescript
// 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

```typescript
await db
  .updateTable('users')
  .set({ name: 'Bob' })
  .where('id', '=', 1)
  .execute();
```

### Delete

```typescript
await db
  .deleteFrom('users')
  .where('id', '=', 1)
  .execute();
```

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:

```typescript
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`:

```typescript
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:

```typescript
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.

## FAQ

### Do Kysely migrations need any extra bundler configuration in a Capacitor app, unlike other ORMs?

No — this is one of the practical advantages of Kysely's approach here. Because the `CapacitorSqliteDialect` implements Kysely's standard `Dialect` interface, migrations are just TypeScript code using the schema builder; there's no generated `.sql` file bundling step or bundler plugin needed, unlike setups that rely on importing raw `.sql` files as strings.

### What does `Generated<number>` actually change about how I use a column?

It changes whether the column is required in `insertInto()` calls versus `selectFrom()` results. A `Generated<number>` primary key becomes optional when inserting (since the database assigns it), but Kysely still types it as present and required on every row you read back — this is purely a TypeScript-level distinction, not something that affects the actual SQL generated.

### Is Kysely closer to a query builder or a full ORM like TypeORM?

A query builder, not an ORM in the traditional sense. Kysely doesn't hide SQL behind entity classes or a repository pattern — its API mirrors SQL syntax directly (`selectFrom`, `where`, `insertInto`), so you're still thinking in terms of tables and columns rather than object relationships. If you want decorator-based entities and automatic relationship loading instead, that's what TypeORM's repository pattern provides.

### What happens if `migrateToLatest()` is called on every app start?

It's safe — the `Migrator` tracks which migrations have already run and only executes pending ones. Repeated calls across app launches won't reapply migrations that already succeeded, so wiring `migrateToLatest()` into your app's startup sequence is the intended usage pattern, not something you need to guard with your own "have migrations run" check.

### Do I need to define relationships in my `Database` interface for joins to work?

Not the way a relational-query ORM requires it. Kysely's `Database` interface only describes column shapes and types per table — foreign key relationships (like `posts.author_id` referencing `users.id`) are expressed in migrations via `.references()`, but querying across tables still means writing an explicit `.innerJoin()` or similar in your query, rather than declaring the relationship once and having Kysely traverse it automatically.

## Stay Updated

Want to stay up to date with the latest features and guides? Subscribe to the Capawesome newsletter.

[Subscribe to the Capawesome Newsletter](/newsletter/){ .md-button .md-button--primary }

## 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.

**Resources:**

- For the full API reference and source code, visit the [Adapter on GitHub](https://github.com/capawesome-team/capacitor-sqlite-drivers/tree/main/packages/kysely){:target="_blank"}

**Related tutorials:**

- 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](./how-to-use-drizzle-orm-with-capacitor-and-sqlite.md)
- For a decorator-based ORM check [TypeORM with Capacitor and SQLite](./how-to-use-typeorm-with-capacitor-and-sqlite.md)


If you have questions or feedback, join the [Capawesome Discord](https://discord.gg/VCXxSVjefW){:target="_blank"} server to connect with the community. And subscribe to the Capawesome [newsletter](/newsletter/){:target="_blank"} to stay updated on the latest news.
