---
title: "SQLite + Drizzle"
description: "Use Drizzle ORM with embedded SQLite in Rivet Actors."
skill: true
---

Use Drizzle when you want typed schema, typed queries, and generated migrations on top of actor-local SQLite.

## What is Drizzle good for?

- **Typed schema**: define tables in TypeScript and get typed query results.
- **Typed query builder**: write SQL-like queries with autocompletion.
- **Migration workflow**: generate SQL migration files from schema changes.
- **Raw SQL escape hatch**: use `c.db.execute(...)` for direct SQLite when needed.

## Project structure

Use one folder per actor database:

```txt
src/
  actors/
    todo-list/
      index.ts
      schema.ts
      drizzle.config.ts
      drizzle/
        0000_init.sql
        migrations.js
        migrations.d.ts
        meta/
          _journal.json
```

- `index.ts` is the actor implementation.
- `drizzle/` contains files managed by `drizzle-kit`.
- Commit generated migration files to source control.

## Basic setup

<CodeGroup workspace>
```json package.json
{
  "scripts": {
    "db:generate": "find src/actors -name drizzle.config.ts -exec drizzle-kit generate --config {} \\;"
  },
  "dependencies": {
    "rivetkit": "*",
    "drizzle-orm": "^0.44.2"
  },
  "devDependencies": {
    "drizzle-kit": "^0.31.2"
  }
}
```

```ts vite.config.ts @nocheck
import { defineConfig, type Plugin } from "vite";
import { readFileSync } from "node:fs";

function sqlRawPlugin(): Plugin {
  return {
    name: "sql-raw",
    transform(_code, id) {
      if (id.endsWith(".sql")) {
        const content = readFileSync(id, "utf-8");
        return { code: `export default ${JSON.stringify(content)};` };
      }
    },
  };
}

export default defineConfig({
  plugins: [sqlRawPlugin()],
});
```

```ts schema.ts
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";

export const todos = sqliteTable("todos", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  createdAt: integer("created_at").notNull(),
});

export const schema = { todos };
```

```ts drizzle.config.ts @nocheck
import { defineConfig } from "rivetkit/db/drizzle";

export default defineConfig({
  schema: "./src/actors/todo-list/schema.ts",
  out: "./src/actors/todo-list/drizzle",
});
```

```sql 0000_init.sql
CREATE TABLE `todos` (
  `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  `title` text NOT NULL,
  `created_at` integer NOT NULL
);
```

```json _journal.json
{
  "version": "7",
  "dialect": "sqlite",
  "entries": [
    {
      "idx": 0,
      "version": "7",
      "when": 1735689600000,
      "tag": "0000_init",
      "breakpoints": true
    }
  ]
}
```

```ts migrations.js @nocheck
import journal from "./meta/_journal.json";
import m0000 from "./0000_init.sql";

export default {
  journal,
  migrations: {
    m0000,
  },
};
```

```ts index.ts @nocheck
import { actor } from "rivetkit";
import { db } from "rivetkit/db/drizzle";
import migrations from "./drizzle/migrations.js";
import { schema, todos } from "./schema.ts";

export const todoList = actor({
  db: db({ schema, migrations }),
  actions: {
    addTodo: async (c, title: string) => {
      const rows = await c.db
        .insert(todos)
        .values({ title, createdAt: Date.now() })
        .returning();
      return rows[0];
    },
    getTodos: async (c) => {
      return await c.db.select().from(todos).orderBy(todos.id);
    },
    getTodoCount: async (c) => {
      const rows = (await c.db.execute(
        "SELECT COUNT(*) AS count FROM todos",
      )) as { count: number }[];
      return rows[0]?.count ?? 0;
    },
  },
});
```

```ts index.ts @nocheck
import { setup } from "rivetkit";
import { todoList } from "./todo-list/index.ts";

export const registry = setup({ use: { todoList } });
registry.start();
```

```ts client.ts @nocheck
import { createClient } from "rivetkit/client";
import type { registry } from "./index";

const client = createClient<typeof registry>("http://localhost:6420");
const todoList = client.todoList.getOrCreate(["main"]);

await todoList.addTodo("Write Drizzle docs");

const todos = await todoList.getTodos();
const count = await todoList.getTodoCount();

console.log(todos, count);
```
</CodeGroup>

## Queries

### Query builder

Use Drizzle's typed query APIs for most reads and writes.

```ts @nocheck
import { eq } from "drizzle-orm";

await c.db.insert(todos).values({ title, createdAt: Date.now() });

const rows = await c.db
  .select()
  .from(todos)
  .where(eq(todos.title, title));
```

### Raw SQL

`rivetkit/db/drizzle` also exposes raw SQLite access through `c.db.execute(...)`.

```ts @nocheck
await c.db.execute(
  "CREATE INDEX IF NOT EXISTS idx_todos_created_at ON todos(created_at)",
);
```

## Queues

Use queues for ordered mutations and keep actions read-only.

```ts @nocheck
queues: {
  addTodo: queue<{ title: string }>(),
},
run: async (c) => {
  for await (const message of c.queue.iter()) {
    if (message.name === "addTodo") {
      await c.db.insert(todos).values({
        title: message.body.title,
        createdAt: Date.now(),
      });
    }
  }
},
actions: {
  getTodos: async (c) => await c.db.select().from(todos),
},
```

## Recommendations

- Prefer Drizzle query APIs for app code and use raw SQL for advanced SQLite features.
- Keep one `drizzle.config.ts` per actor folder.
- Re-run `db:generate` after schema changes and commit generated migration files.
- Use queues for writes and actions for reads.
- Keep related writes in one action or queue message to reduce interleaved query risk.

## Read more

- [Drizzle SQLite quickstart](https://orm.drizzle.team/docs/get-started-sqlite)
- [Drizzle `drizzle-kit generate`](https://orm.drizzle.team/docs/drizzle-kit-generate)
- [Drizzle + Cloudflare D1](https://orm.drizzle.team/docs/connect-cloudflare-d1)
- [Drizzle + Cloudflare Durable Objects](https://orm.drizzle.team/docs/connect-cloudflare-do)
- [Cloudflare Durable Objects SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/)
