website/src/content/docs/actors/sqlite.mdx
For a high-level overview of where to store actor data, including when to use c.state versus SQLite, see State & Storage. For a worked multi-tenant pattern, see the cookbook: Database per Tenant.
WHERE, LIKE, and ORDER BY instead of manual in-memory loops.JOIN queries for connected data.Rivet supports both raw SQL and Drizzle for actor-local SQLite.
Use raw SQL when you want direct query control and minimal abstraction.
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
const rows = await c.db.execute("SELECT id, title FROM todos ORDER BY id DESC");
Use Drizzle when you want typed schema and typed query APIs.
await c.vars.drizzle.insert(todos).values({ title });
const rows = await c.vars.drizzle.select().from(todos).orderBy(desc(todos.id));
You can mix both in the same actor.
For Drizzle setup, see SQLite + Drizzle.
Define db: db({ onMigrate }) on your actor, create your schema in onMigrate, and execute SQL with c.db.execute(...).
RivetKit wraps onMigrate in a SQLite savepoint, so migration steps are atomic. If onMigrate throws, all SQL run by that hook is rolled back before the actor starts.
c.db.execute(...) returns an array of row objects for SELECT queries.
const rows = await c.db.execute(
"SELECT id, title FROM todos WHERE title LIKE ?",
`%${query}%`,
);
Use ? placeholders for dynamic values and pass parameters in order after the SQL string.
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
You can also use named SQLite bindings by passing a single properties object.
const rows = await c.db.execute(
"SELECT id, title FROM todos WHERE title = :title",
{ title: "Write SQLite docs" },
);
Use transactions when multiple writes must succeed or fail together.
BEGIN.COMMIT if all queries succeed.ROLLBACK and rethrow.c.db connection. Be careful with other interleaved queries.await c.db.execute("BEGIN");
try {
await c.db.execute("INSERT INTO todos (title) VALUES (?)", title);
await c.db.execute(
"INSERT INTO comments (todo_id, body) VALUES (last_insert_rowid(), ?)",
body,
);
await c.db.execute("COMMIT");
} catch (error) {
await c.db.execute("ROLLBACK");
throw error;
}
It's recommended to use queues for mutations and actions for read-only queries. This is the same code structure as the basic setup, but mutation writes are routed through queues.
<CodeGroup> <CodeSnippet file="examples/docs/actors-sqlite/queues/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-sqlite/queues/client.ts" title="client.ts" /> </CodeGroup>GET /inspector/summary includes isDatabaseEnabled so you can confirm SQLite is configured.GET /inspector/database/schema returns the tables and views discovered in the actor's SQLite database.GET /inspector/database/rows?table=...&limit=100&offset=0 returns paged rows for a specific table or view.POST /inspector/database/execute lets you run ad-hoc SQL for debugging and data fixes with positional args or named properties.onMigrate; RivetKit runs them atomically inside a SQLite savepoint.? placeholders for dynamic values.