KV
Migrate actors off the deprecated c.kv key-value store to SQLite.
Actor KV was deprecated in 3.0 and will be removed in a future version. It still works today, so you can migrate at your own pace.
What is deprecated
c.kv is a low-level key-value store on every actor. All of its methods are deprecated: get, put, delete, list, listPrefix, listRange, batchGet, batchPut, batchDelete and deleteRange. In Rust, Ctx::kv is deprecated.
import { actor } from "rivetkit";
// An actor that still uses the deprecated `c.kv`
const preferences = actor({
actions: {
set: async (c, userId: string, theme: string) => {
await c.kv.put(`theme:${userId}`, theme);
},
get: async (c, userId: string) => {
return await c.kv.get(`theme:${userId}`);
},
list: async (c) => {
return await c.kv.list("theme:");
},
},
});
Store the same data in SQLite (c.db) instead. A table with a primary key covers every KV pattern, and SQLite adds indexes, queries and transactions.
Replace KV with a table
| KV | SQLite |
|---|---|
c.kv.put(key, value) | INSERT ... ON CONFLICT (key) DO UPDATE |
c.kv.get(key) | SELECT ... WHERE key = ? |
c.kv.delete(key) | DELETE ... WHERE key = ? |
c.kv.list(prefix) | SELECT ... WHERE key >= ? AND key < ?, or a separate table per prefix |
c.kv.batchPut(entries) | Several statements in c.db.transaction |
| Binary values | A BLOB column |
Most KV prefixes map to their own table, with the rest of the key as the primary key:
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
const preferences = actor({
db: db({
onMigrate: async (db) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS themes (
user_id TEXT PRIMARY KEY,
theme TEXT NOT NULL
)
`);
},
}),
actions: {
// `put` becomes an upsert
set: async (c, userId: string, theme: string) => {
await c.db.execute(
"INSERT INTO themes (user_id, theme) VALUES (?, ?) ON CONFLICT (user_id) DO UPDATE SET theme = excluded.theme",
userId,
theme,
);
},
// `get` becomes a lookup by primary key
get: async (c, userId: string) => {
const rows = await c.db.execute<{ theme: string }>(
"SELECT theme FROM themes WHERE user_id = ?",
userId,
);
return rows[0]?.theme ?? null;
},
// `list` by prefix becomes a query over the whole table
list: async (c) => {
return await c.db.execute<{ user_id: string; theme: string }>(
"SELECT user_id, theme FROM themes ORDER BY user_id",
);
},
},
});
Migrate existing data
Copy existing KV entries into SQLite the next time each actor starts:
- Create the new tables and a
migrationstable inonMigrate. - In
onWake, skip the copy if it already ran, otherwise read the old entries withc.kv.listand upsert them. - Record the copy in
migrationsso it runs once.
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
const preferences = actor({
db: db({
onMigrate: async (db) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS themes (
user_id TEXT PRIMARY KEY,
theme TEXT NOT NULL
)
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS migrations (
name TEXT PRIMARY KEY
)
`);
},
}),
// `onWake` runs on every start before the actor handles requests
onWake: async (c) => {
const done = await c.db.execute(
"SELECT 1 FROM migrations WHERE name = 'kv-themes'",
);
if (done.length > 0) return;
// Copy every KV entry under the old prefix. Returned keys still include
// the prefix, so strip it to get the user ID.
const entries = await c.kv.list("theme:");
for (const [key, theme] of entries) {
const userId = key.slice("theme:".length);
await c.db.execute(
"INSERT INTO themes (user_id, theme) VALUES (?, ?) ON CONFLICT (user_id) DO UPDATE SET theme = excluded.theme",
userId,
theme,
);
}
// Mark the copy as done so it runs once
await c.db.execute("INSERT INTO migrations (name) VALUES ('kv-themes')");
},
actions: {
get: async (c, userId: string) => {
const rows = await c.db.execute<{ theme: string }>(
"SELECT theme FROM themes WHERE user_id = ?",
userId,
);
return rows[0]?.theme ?? null;
},
},
});
The upsert makes the copy safe to repeat, so a crash partway through retries it on the next start. Once every actor has started on the new version, remove the copy and stop reading from c.kv.
For binary keys or values, pass keyType: "binary" and type: "binary" to c.kv.list and store them in BLOB columns.
Entries written with c.kv.batchPut are not returned by c.kv.list. Read them back with c.kv.batchGet using the same keys.