More
SQLite + Drizzle
Use Drizzle ORM with embedded SQLite in Rivet Actors.
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:
src/
actors/
todo-list/
index.ts
schema.ts
drizzle.config.ts
drizzle/
0000_init.sql
migrations.js
migrations.d.ts
meta/
_journal.json
index.tsis the actor implementation.drizzle/contains files managed bydrizzle-kit.- Commit generated migration files to source control.
Basic setup
{
"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"
}
}
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()],
});
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 };
import { defineConfig } from "rivetkit/db/drizzle";
export default defineConfig({
schema: "./src/actors/todo-list/schema.ts",
out: "./src/actors/todo-list/drizzle",
});
CREATE TABLE `todos` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`title` text NOT NULL,
`created_at` integer NOT NULL
);
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1735689600000,
"tag": "0000_init",
"breakpoints": true
}
]
}
import journal from "./meta/_journal.json";
import m0000 from "./0000_init.sql";
export default {
journal,
migrations: {
m0000,
},
};
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;
},
},
});
import { setup } from "rivetkit";
import { todoList } from "./todo-list/index.ts";
export const registry = setup({ use: { todoList } });
import { createClient } from "rivetkit/client";
import type { registry } from "./actors/index";
const client = createClient<typeof registry>();
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);
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)",
);
Queues
Use queues for ordered mutations and keep actions read-only.
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.tsper actor folder. - Re-run
db:generateafter 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.