Skip to main content
More

SQLite + Drizzle

Use Drizzle ORM with a Rivet Actor's embedded SQLite database for typed schemas, queries, migrations, and durable relational data.

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

For a high-level overview of where to store actor data, see State & Storage.

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:

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/ holds the SQL migrations (*.sql) and meta/_journal.json generated by drizzle-kit.
  • migrations.js is a small RivetKit glue file you maintain by hand. It imports the journal and each *.sql file and exports a { journal, migrations } object keyed by migration (for example m0000). Add a new entry here whenever db:generate produces a new migration.
  • Commit the generated migration files and migrations.js to source control.

Basic setup

Queries

Query builder

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

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(...).

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

Transactions

Actions run concurrently and interleave at every await, so make each write a single statement or group related writes in c.db.transaction. Pass a static name, use only the callback’s tx inside it, and make network calls after it commits. See Transactions.

import { eq } from "drizzle-orm";

// ...

actions: {
  // The check and the update run together, so two renames cannot both claim a title
  renameTodo: async (c, id: number, title: string) => {
    return await c.db.transaction(
      async (tx) => {
        const taken = await tx.select().from(todos).where(eq(todos.title, title));
        if (taken.length > 0) return false;

        await tx.update(todos).set({ title }).where(eq(todos.id, id));
        return true;
      },
      { name: "rename-todo" },
    );
  },
},

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.
  • Keep related writes in one c.db.transaction, because concurrent actions can interleave between separate queries.

Read more

Edit this page Last updated November 10, 2026