---
title: How to Use Drizzle ORM with Capacitor and SQLite
description: Learn how to use Drizzle ORM with the Capacitor SQLite plugin to build type-safe database layers in your cross-platform mobile apps.
date:
  created: 2026-02-25
  updated: 2026-07-17
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor SQLite: sdks/capacitor/sqlite.md
faq: true
---

# How to Use Drizzle ORM with Capacitor and SQLite

If you're building a Capacitor app that needs a local database, you've probably dealt with writing raw SQL strings and mapping results manually. It works, but it's error-prone and doesn't scale well. Drizzle ORM offers a better approach: a lightweight, type-safe ORM that lets you define your schema in TypeScript and write queries that feel like SQL, with full autocompletion and compile-time checks. In this guide, you'll learn how to set up Drizzle ORM with the [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md)  using the new `@capawesome/capacitor-sqlite-drizzle` adapter. For the Capacitor SQLite plugin itself, 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 Drizzle ORM?

[Drizzle ORM](https://orm.drizzle.team/){:target="_blank"} is a TypeScript ORM designed to be lightweight, type-safe, and close to SQL. Unlike heavier ORMs that abstract SQL away entirely, Drizzle uses a query API that mirrors SQL syntax. If you know SQL, you already know most of Drizzle.

Here's what makes it a good fit for Capacitor apps:

- **Type safety**: Your schema is defined in TypeScript, so queries are checked at compile time. No more runtime surprises from mistyped column names.
- **No code generation**: Unlike some ORMs, Drizzle doesn't require a separate code generation step. Your schema files are regular TypeScript.
- **SQL-like syntax**: Queries read like SQL, which makes them easy to understand and debug.
- **Built-in migrations**: Drizzle Kit can generate and manage SQL migrations from your schema changes automatically.
- **Lightweight**: Drizzle has zero runtime dependencies and a small bundle size, which matters for mobile apps.

## 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. New to the plugin itself? [How to Use SQLite in a Capacitor App](./how-to-use-sqlite-in-a-capacitor-app.md) walks through the raw database API that Drizzle builds on.

## Installation

Install the Drizzle adapter along with Drizzle ORM:

```bash
npm install @capawesome/capacitor-sqlite-drizzle drizzle-orm
```

You'll also want to install Drizzle Kit as a dev dependency for schema migrations:

```bash
npm install -D drizzle-kit
```

## Setting Up the Database

To get started, open a database using the [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md) and pass it to the `drizzle()` function:

```typescript
import { Sqlite } from '@capawesome-team/capacitor-sqlite';
import { drizzle } from '@capawesome/capacitor-sqlite-drizzle';

const { databaseId } = await Sqlite.open({ path: 'my.db' });
const db = drizzle(Sqlite, { databaseId });
```

The `drizzle()` function accepts the `Sqlite` plugin instance and a configuration object. The `databaseId` is the unique identifier returned by [`open(...)`](../../sdks/capacitor/sqlite.md#open) and is required to route queries to the correct database.

You can also pass additional Drizzle configuration options like `schema` and `logger`:

```typescript
import * as schema from './schema';

const db = drizzle(Sqlite, { databaseId, schema, logger: true });
```

Passing `schema` enables Drizzle's relational query API (more on that later), and `logger: true` logs all executed SQL statements to the console, which is useful during development.

## Defining a Schema

Drizzle uses a schema-as-code approach. You define your tables as TypeScript objects, which Drizzle uses for type inference and query building. Create a `schema.ts` file in your project:

```typescript
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
});

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id')
    .notNull()
    .references(() => users.id),
});
```

A few things to note here:

- `sqliteTable` defines a table with its columns and constraints.
- Column types like `integer` and `text` map directly to SQLite types.
- The `mode: 'timestamp'` option on `createdAt` tells Drizzle to automatically convert between JavaScript `Date` objects and integer timestamps.
- `references(() => users.id)` creates a foreign key constraint linking `posts.authorId` to `users.id`.
- `$defaultFn` sets a default value at the application level, not in the database.

This schema definition is the single source of truth for both your TypeScript types and your database structure.

## Running Queries

With the schema in place, you can run type-safe CRUD operations. All queries return promises and use the familiar SQL patterns.

### Insert

Insert a row with `db.insert(...)` and `values(...)`:

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

### Select

Read rows with `db.select()`, filtered with `where(...)` and the `eq` helper:

```typescript
import { eq } from 'drizzle-orm';

// Select all users
const allUsers = await db.select().from(users);

// Select with a filter
const user = await db
  .select()
  .from(users)
  .where(eq(users.email, 'alice@example.com'));
```

### Update

Change existing rows with `db.update(...)` and `set(...)`:

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

### Delete

Remove rows with `db.delete(...)`:

```typescript
await db.delete(users).where(eq(users.id, 1));
```

Every query is fully typed. The `allUsers` variable, for example, is automatically inferred as an array of objects matching the `users` table schema — no manual type annotations needed.

## Relational Queries

When you pass a `schema` to the `drizzle()` function, you can use Drizzle's relational query API. This lets you load related data in a single query without writing manual joins:

```typescript
const usersWithPosts = await db.query.users.findMany({
  with: { posts: true },
});
```

This returns all users along with their associated posts, based on the foreign key relationship defined in the schema. The result is fully typed and nested. Each user object includes a `posts` array.

You can also use `findFirst` to retrieve a single record:

```typescript
const user = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: { posts: true },
});
```

## Transactions

For operations that need to succeed or fail together, use transactions. Drizzle sends `BEGIN`, `COMMIT`, and `ROLLBACK` statements automatically:

```typescript
await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ name: 'Alice', email: 'alice@example.com' })
    .returning();
  await tx
    .insert(posts)
    .values({ title: 'Hello World', content: '...', authorId: user.id });
});
```

If any statement inside the callback throws an error, the entire transaction is rolled back.

## Migrations

Manually managing database schema changes with raw SQL is tedious and error-prone. Drizzle Kit solves this by generating SQL migration files from your schema changes. The adapter provides a `migrate()` function to apply these migrations at runtime.

### 1. Configure Drizzle Kit

Create a `drizzle.config.ts` file in your project root:

```typescript
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/schema.ts',
  out: './src/drizzle',
  dialect: 'sqlite',
  driver: 'expo',
});
```

!!! note
    The `driver: 'expo'` setting is required. It tells Drizzle Kit to generate a bundled `migrations.js` file that works in non-Node environments like Capacitor.

### 2. Generate Migrations

Whenever you change your schema, run the following command to generate migration files:

```bash
npx drizzle-kit generate
```

This creates SQL migration files and a `migrations.js` bundle in the output directory (e.g. `./src/drizzle/`).

!!! note
    The generated `migrations.js` file imports `.sql` files as strings. Depending on your bundler, you may need a plugin to handle this. For Vite-based projects, use [`vite-plugin-plain-text`](https://www.npmjs.com/package/vite-plugin-plain-text){:target="_blank"}. For Babel-based setups, use [`babel-plugin-inline-import`](https://www.npmjs.com/package/babel-plugin-inline-import){:target="_blank"}.

### 3. Apply Migrations

Import the generated migrations and apply them when your app starts:

```typescript
import { Sqlite } from '@capawesome-team/capacitor-sqlite';
import { drizzle, migrate } from '@capawesome/capacitor-sqlite-drizzle';
import migrations from './drizzle/migrations';

const { databaseId } = await Sqlite.open({ path: 'my.db' });
const db = drizzle(Sqlite, { databaseId });

await migrate(Sqlite, databaseId, migrations);
```

Each migration runs inside its own transaction. The adapter automatically creates a `__drizzle_migrations` table to track which migrations have already been applied, so calling `migrate()` multiple times is safe. Only pending migrations are executed.

## FAQ

### Why does `drizzle.config.ts` need `driver: 'expo'` for a Capacitor app?

Because that setting controls how Drizzle Kit packages migrations, not which runtime executes them. `driver: 'expo'` tells Drizzle Kit to bundle a `migrations.js` file with the SQL embedded as importable strings, instead of assuming Node.js filesystem access to read `.sql` files at runtime. A Capacitor app's WebView needs the same bundled format, even though it isn't Expo.

### Do I need the `schema` option every time I call `drizzle()`, or only for certain features?

Only if you want the relational query API (`db.query.users.findMany({ with: {...} })`). Passing `schema` is what enables Drizzle to resolve relationships and return nested typed results; without it, you can still run standard `select`/`insert`/`update`/`delete` queries, just without the `db.query` convenience layer.

### What happens if I run `migrate()` more than once?

Nothing destructive — it's designed to be safe to call on every app start. The adapter tracks applied migrations in a `__drizzle_migrations` table it creates automatically, so `migrate()` only executes migrations that haven't run yet, rather than reapplying the whole history.

### Why does importing the generated `migrations.js` file sometimes fail to build?

Because it imports `.sql` files as raw strings, which most bundlers don't handle without help. Vite projects need `vite-plugin-plain-text`, and Babel-based setups need `babel-plugin-inline-import`. Without one of these, the build fails on the `.sql` imports inside the generated migrations bundle rather than on anything in your own code.

### Can I use Drizzle's relational queries without defining foreign keys in my schema?

No — the relational query API depends on the `references()` calls in your schema to know how tables connect. If `posts.authorId` doesn't reference `users.id` in the schema definition, `db.query.users.findMany({ with: { posts: true } })` has no relationship to follow, and you'd need to fall back to a manual `select` with a `where` clause joining the tables yourself.

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

Reach for Drizzle when you want one TypeScript definition to drive both your types and your SQLite schema, and you are comfortable writing queries that look like SQL. To get started, create `schema.ts`, run `npx drizzle-kit generate`, and call `migrate()` on app start before your first query.

**Resources and related readings:**

- [Adapter on GitHub](https://github.com/capawesome-team/capacitor-sqlite-drivers/tree/main/packages/drizzle){:target="_blank"}
- [API Reference](../../sdks/capacitor/sqlite.md#api)
- [Exploring the Capacitor SQLite API](./exploring-the-capacitor-sqlite-api.md)
- For decorator-based ORMs, make sure to read [TypeORM](./how-to-use-typeorm-with-capacitor-and-sqlite.md)
- For a query-builder approach check [Kysely](./how-to-use-kysely-with-capacitor-and-sqlite.md)

Join the Capawesome [Discord](https://discord.gg/VCXxSVjefW){:target="_blank"} server for questions and subscribe to the Capawesome [newsletter](/newsletter/){:target="_blank"} to stay updated.
