---
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, which we'll define next.

## Defining Your Database Types

Kysely uses TypeScript interfaces to describe your database schema. 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

Insert a row with `insertInto(...)` and `values(...)`:

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

### Select

Read rows with `selectFrom(...)`, optionally filtered with `where(...)`:

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

Change existing rows with `updateTable(...)` and `set(...)`:

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

### Delete

Remove rows with `deleteFrom(...)`:

```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, so related records across multiple tables are never written half-way.

## 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, with 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. 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. The distinction is purely at the TypeScript level and does not change the generated SQL.

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

Use Kysely if you want to keep writing SQL and have the compiler check it for you. If you would rather describe your schema once and query it through entities or relations, one of the alternatives below is the better fit. To get started, install `@capawesome/capacitor-sqlite-kysely`, define your `Database` interface, and call `migrateToLatest()` on app start.

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