# Low-Level KV Storage

> **NOTE:** KV is deprecated. It is a low-level escape hatch kept for backward compatibility. For new actors, prefer [in-memory state](/actors/docs/state) for small serializable values or [SQLite](/actors/docs/sqlite) for larger or queryable data.

Every Rivet Actor includes a lightweight key-value store on `c.kv`. It is useful for dynamic keys, blobs, or data that does not fit well in structured state.

If your data has a known schema, prefer [state](/actors/docs/state). KV is best for flexible or user-defined keys.

## Basic Usage

Keys and values default to `text`, so you can use strings without extra options.

examples/docs/actors-kv/basic-usage.ts:

```ts
import { actor } from "rivetkit";

const greetings = actor({
	state: {},
	actions: {
		setGreeting: async (c, userId: string, message: string) => {
			await c.kv.put(`greeting:${userId}`, message);
		},
		getGreeting: async (c, userId: string) => {
			return await c.kv.get(`greeting:${userId}`);
		},
	},
});
```

## Value Types

You can store binary values by passing `Uint8Array` or `ArrayBuffer`. Use `type` on both reads and writes to get the right value type: `binary` for `Uint8Array` and `arrayBuffer` for `ArrayBuffer`.

examples/docs/actors-kv/value-types.ts:

```ts
import { actor } from "rivetkit";

const assets = actor({
	state: {},
	actions: {
		putAvatar: async (c, bytes: Uint8Array) => {
			await c.kv.put("avatar", bytes);
		},
		getAvatar: async (c) => {
			return await c.kv.get("avatar", { type: "binary" });
		},
		putSnapshot: async (c, data: ArrayBuffer) => {
			await c.kv.put("snapshot", data, { type: "arrayBuffer" });
		},
		getSnapshot: async (c) => {
			return await c.kv.get("snapshot", { type: "arrayBuffer" });
		},
	},
});
```

TypeScript returns a concrete type based on the option you pass in:

examples/docs/actors-kv/typed-returns.ts:

```ts
import { actor } from "rivetkit";

const example = actor({
	state: {},
	actions: {
		demo: async (c) => {
			const textValue = await c.kv.get("greeting");
			//    ^? string | null

			const bytes = await c.kv.get("avatar", { type: "binary" });
			//    ^? Uint8Array | null
		},
	},
});
```

## Key Types

Keys accept either `string` or `Uint8Array`. String keys are encoded as UTF-8 by default.

When listing by prefix, you can control how keys are decoded with `keyType`. Returned keys have the prefix removed.

examples/docs/actors-kv/key-types.ts:

```ts
import { actor } from "rivetkit";

const example = actor({
	state: {},
	actions: {
		listGreetings: async (c) => {
			const results = await c.kv.list("greeting:", { keyType: "text" });

			for (const [key, value] of results) {
				console.log(key, value);
			}
		},
	},
});
```

If you use binary keys, set `keyType: "binary"` so the returned keys stay as `Uint8Array`.

## Range Operations

Use `listRange(start, end)` to read an arbitrary half-open range `[start, end)`. Use `deleteRange(start, end)` to clear that same range efficiently.

examples/docs/actors-kv/range-operations.ts:

```ts
import { actor } from "rivetkit";

const example = actor({
	state: {},
	actions: {
		pruneAndScan: async (c) => {
			const active = await c.kv.listRange("job:", "joc:", {
				keyType: "text",
			});

			const encoder = new TextEncoder();
			await c.kv.deleteRange(
				encoder.encode("job:old:"),
				encoder.encode("job:old;"),
			);

			return active.map(([key, value]) => ({ key, value }));
		},
	},
});
```

## Batch Operations

KV supports batch operations for efficiency. `batchPut` and `batchGet` work on raw `Uint8Array` keys and values, so encode strings before passing them in.

examples/docs/actors-kv/batch-operations.ts:

```ts
import { actor } from "rivetkit";

const example = actor({
	state: {},
	actions: {
		batchOps: async (c) => {
			const encoder = new TextEncoder();

			await c.kv.batchPut([
				[encoder.encode("alpha"), encoder.encode("1")],
				[encoder.encode("beta"), encoder.encode("2")],
			]);

			const values = await c.kv.batchGet([
				encoder.encode("alpha"),
				encoder.encode("beta"),
			]);
		},
	},
});
```
