---
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 — 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 serves as 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

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

### Select

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

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

### 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 unlock 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. This is essential for maintaining data integrity, especially when inserting related records across multiple tables.

## 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 actually 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 — which is exactly what a Capacitor app's WebView also needs, 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

With the `@capawesome/capacitor-sqlite-drizzle` adapter, you can use Drizzle ORM's type-safe queries, schema-as-code approach, and automated migrations in your Capacitor apps. The setup is straightforward: define your schema in TypeScript, generate migrations with Drizzle Kit, and run queries using a familiar SQL-like API — all without sacrificing type safety.

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