diff --git a/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/image.png b/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/image.png new file mode 100644 index 0000000000..b711177a74 Binary files /dev/null and b/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/image.png differ diff --git a/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/page.mdx b/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/page.mdx new file mode 100644 index 0000000000..f62cb12498 --- /dev/null +++ b/website/src/content/posts/2026-02-25-sqlite-for-rivet-actors/page.mdx @@ -0,0 +1,397 @@ +--- +author: nathan-flurry +published: "2026-02-25" +category: changelog +keywords: ["sqlite", "actors", "drizzle", "realtime", "edge", "multi-tenancy", "open-source", "typescript"] +title: "Introducing SQLite for Rivet Actors" +description: "Millions of isolated SQLite databases that scale to zero, run at the edge, and stream changes to your React frontend in realtime." +--- + +Today we're releasing **[SQLite for Rivet Actors](/docs/actors/sqlite)**: embedded, per-actor SQLite databases that run at the edge. + +- **Massive multi-tenancy**: one database per actor, millions of isolated databases for affordable multi-tenancy +- **Scales to zero**: idle databases cost nothing +- **Zero-latency reads**: compute and storage co-located, no network hops +- **Runs at the edge**: databases live near your users, not in a distant region +- **First-class Drizzle ORM support**: typed schema, typed queries, generated migrations +- **Realtime React**: stream database changes to your frontend with `useActor` +- **The only open-source SQLite multi-tenant platform**: managed SQLite with millions of databases per project, Apache 2.0, deploy anywhere + +## Show Me The Code + +Define `db` on your actor and you get a full SQLite database scoped to that actor instance. No connection strings, no provisioning, no config. + + +```ts Basic +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +const todoList = actor({ + // Each actor instance gets its own SQLite database + db: db({ + // Create tables on first run + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 + ); + `); + }, + }), + actions: { + // Parameterized queries with ? placeholders + addTodo: async (c, title: string) => { + await c.db.execute("INSERT INTO todos (title) VALUES (?)", title); + }, + toggleTodo: async (c, id: number) => { + await c.db.execute( + "UPDATE todos SET completed = NOT completed WHERE id = ?", + id, + ); + }, + // SELECT returns typed row objects + getTodos: async (c) => { + return (await c.db.execute( + "SELECT id, title, completed FROM todos ORDER BY id DESC", + )) as { id: number; title: string; completed: number }[]; + }, + }, +}); + +export const registry = setup({ use: { todoList } }); +``` + +```ts Drizzle +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db/drizzle"; +import { integer, sqliteTable, text } from "rivetkit/db/drizzle"; +import { eq } from "drizzle-orm"; +// Auto-generated by `drizzle-kit generate` +import migrations from "./drizzle/migrations.js"; + +// Define typed schema +const todos = sqliteTable("todos", { + id: integer("id").primaryKey({ autoIncrement: true }), + title: text("title").notNull(), + completed: integer("completed").notNull().default(0), +}); + +const schema = { todos }; + +const todoList = actor({ + // Drizzle handles migrations automatically from generated files + db: db({ schema, migrations }), + actions: { + addTodo: async (c, title: string) => { + // Typed insert with .returning() + const rows = await c.db.insert(todos).values({ title }).returning(); + return rows[0]; + }, + toggleTodo: async (c, id: number) => { + // Typed select with .where() + const current = await c.db + .select() + .from(todos) + .where(eq(todos.id, id)); + if (current.length > 0) { + await c.db + .update(todos) + .set({ completed: current[0].completed ? 0 : 1 }) + .where(eq(todos.id, id)); + } + }, + getTodos: async (c) => { + // Typed results, no manual casting + return await c.db.select().from(todos).orderBy(todos.id); + }, + }, +}); + +export const registry = setup({ use: { todoList } }); +``` + +```ts Queues +import { actor, queue, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +const todoList = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 + ); + `); + }, + }), + // Typed queues for ordered, durable writes + queues: { + addTodo: queue<{ title: string }>(), + toggleTodo: queue<{ id: number }>(), + }, + // Process queue messages sequentially + run: async (c) => { + for await (const message of c.queue.iter()) { + if (message.name === "addTodo") { + await c.db.execute( + "INSERT INTO todos (title) VALUES (?)", + message.body.title, + ); + } else if (message.name === "toggleTodo") { + await c.db.execute( + "UPDATE todos SET completed = NOT completed WHERE id = ?", + message.body.id, + ); + } + } + }, + // Keep actions read-only + actions: { + getTodos: async (c) => { + return (await c.db.execute( + "SELECT id, title, completed FROM todos ORDER BY id DESC", + )) as { id: number; title: string; completed: number }[]; + }, + }, +}); + +export const registry = setup({ use: { todoList } }); +``` + +```ts Transactions +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +const ledger = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + balance INTEGER NOT NULL DEFAULT 0 + ); + `); + await db.execute(` + CREATE TABLE IF NOT EXISTS transfers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_id TEXT NOT NULL, + to_id TEXT NOT NULL, + amount INTEGER NOT NULL + ); + `); + }, + }), + actions: { + // Atomic transfer: debit, credit, and log succeed or fail together + transfer: async (c, fromId: string, toId: string, amount: number) => { + await c.db.execute("BEGIN"); + try { + // Debit sender + await c.db.execute( + "UPDATE accounts SET balance = balance - ? WHERE id = ?", + amount, + fromId, + ); + // Credit receiver + await c.db.execute( + "UPDATE accounts SET balance = balance + ? WHERE id = ?", + amount, + toId, + ); + // Log the transfer + await c.db.execute( + "INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)", + fromId, + toId, + amount, + ); + await c.db.execute("COMMIT"); + } catch (error) { + // Roll back all changes on failure + await c.db.execute("ROLLBACK"); + throw error; + } + }, + getBalance: async (c, accountId: string) => { + const rows = (await c.db.execute( + "SELECT balance FROM accounts WHERE id = ?", + accountId, + )) as { balance: number }[]; + return rows[0]?.balance ?? 0; + }, + }, +}); + +export const registry = setup({ use: { ledger } }); +``` + + +## Realtime React Integration + +Combine SQLite with actor events and `useActor` to build realtime apps. Writes persist to SQLite and broadcast to every connected client instantly. + + +```ts actors.ts +import { actor, event, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +interface Todo { + id: number; + title: string; + completed: number; +} + +export const todoList = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 + ); + `); + }, + }), + // Typed events that clients can subscribe to + events: { + todosUpdated: event(), + }, + actions: { + addTodo: async (c, title: string) => { + // Write to SQLite + await c.db.execute("INSERT INTO todos (title) VALUES (?)", title); + // Broadcast updated list to all connected clients + const todos = (await c.db.execute( + "SELECT id, title, completed FROM todos ORDER BY id DESC", + )) as Todo[]; + c.broadcast("todosUpdated", todos); + }, + toggleTodo: async (c, id: number) => { + await c.db.execute( + "UPDATE todos SET completed = NOT completed WHERE id = ?", + id, + ); + const todos = (await c.db.execute( + "SELECT id, title, completed FROM todos ORDER BY id DESC", + )) as Todo[]; + c.broadcast("todosUpdated", todos); + }, + getTodos: async (c) => { + return (await c.db.execute( + "SELECT id, title, completed FROM todos ORDER BY id DESC", + )) as Todo[]; + }, + }, +}); + +export const registry = setup({ use: { todoList } }); +``` + +```tsx App.tsx +import { useState, useEffect } from "react"; +import { createRivetKit } from "@rivetkit/react"; +import type { registry } from "./actors"; + +const { useActor } = createRivetKit(); + +function TodoApp() { + // Connect to the actor over WebSockets + const { connection, connStatus, useEvent } = useActor({ + name: "todoList", + key: ["my-list"], + }); + const [todos, setTodos] = useState< + { id: number; title: string; completed: number }[] + >([]); + const [input, setInput] = useState(""); + + // Load initial data + useEffect(() => { + if (!connection) return; + connection.getTodos().then(setTodos); + }, [connection]); + + // Live updates from the actor + useEvent("todosUpdated", setTodos); + + if (connStatus !== "connected") return
Connecting...
; + + return ( +
+
{ + e.preventDefault(); + connection!.addTodo(input); + setInput(""); + }} + > + setInput(e.target.value)} /> + +
+
    + {todos.map((todo) => ( +
  • connection!.toggleTodo(todo.id)} + style={{ + textDecoration: todo.completed ? "line-through" : "none", + cursor: "pointer", + }} + > + {todo.title} +
  • + ))} +
+
+ ); +} +``` +
+ +## Built-in Database Explorer + +TODO + +## Massive Multi-Tenancy + +Every actor instance gets **its own SQLite database**. Spin up millions of isolated databases, one per user, one per workspace, one per document. No connection pools, no row-level security, no per-tenant configuration. + +## Zero-Latency Reads + +Compute and storage live on the same machine. Queries read from **local memory**, not over the network to an external database. No connection overhead, no round trips. + +## Scale to Zero + +Actors **hibernate when idle**. A database with no traffic costs nothing. When a request arrives, the actor wakes up with its full SQLite database intact. You only pay for what you use. + +## At the Edge, Realtime by Default + +Rivet Actors run **at the edge, close to your users**. Combined with actor events and WebSockets, you get a realtime data layer with no additional infrastructure. Write to SQLite, broadcast the change, and every connected client sees it instantly. + +## Open Source, Deploy Anywhere + +Rivet is **the only multi-tenant SQLite platform that's open-source**. Apache 2.0. Deploy on Cloudflare Workers, Vercel, Railway, or your own infrastructure. No vendor lock-in. + +Plus everything else that comes with [Rivet Actors](/docs/actors): scale to millions of instances, TypeScript-native, WebSocket events, queues, state, and workflows. + +## Get Started + +SQLite is available today in RivetKit. + +```bash +npm install rivetkit +``` + +```ts +import { db } from "rivetkit/db"; +``` + +- [SQLite documentation](/docs/actors/sqlite) +- [SQLite + Drizzle documentation](/docs/actors/sqlite-drizzle) +- [GitHub](https://github.com/rivet-dev/rivet) +- [Discord](https://rivet.dev/discord)