npm package: @mofax/cfkv
Description: Typed Cloudflare KV client with a table-style API built on tuple keys.
Latest version: 0.0.0

# cfkv

Typed Cloudflare KV client with a table-style API built on tuple keys.

`cfkv` wraps the Cloudflare KV REST API and gives you:

- typed reads and writes based on a schema
- automatic namespace lookup and creation
- tuple keys like `["users", "user_1"]` instead of manual string concatenation
- batch operations for writes and reads
- full-table scans, cursor pagination, and async iteration

## What This Library Does

Cloudflare KV is a flat key-value store. This library adds a lightweight structure on top:

- a logical namespace is resolved from `namespaceName` and `dbName`
- the first tuple key segment is treated as a table name
- the remaining tuple key segments form the row key

Example:

- `namespace("app", "prod")` resolves to a Cloudflare namespace titled `prod:app`
- `["users", "user_1"]` is stored as the KV key `prod:users:user_1`

This gives you a simple table-like model without adding a database server or query layer.

## Installation

This repo currently builds the library from source. To work with it locally:

```bash
bun install
```

The package exports ESM entrypoints and TypeScript declarations. Consumer code imports it as:

```ts
import { CfKV } from "cfkv";
```

## Requirements

You need:

- a Cloudflare account ID
- a Cloudflare API token with Workers KV permissions

Pass them to the client:

```ts
const cfkv = new CfKV({
	accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
	apiToken: process.env.CLOUDFLARE_API_TOKEN!,
});
```

You can also provide:

- `baseUrl` to override the Cloudflare API base URL
- `fetch` to inject a custom fetch implementation
- `headers` to merge custom headers into every request

## Core Concepts

### Schema

The schema maps table names to JSON value types:

```ts
type AppSchema = {
	users: { name: string; age: number };
	sessions: { userId: string; createdAt: string };
};
```

### Namespace Handle

Create a typed namespace handle by passing your schema to `namespace()`:

```ts
const kv = await cfkv.namespace<AppSchema>("app", "prod");
```

On first use, the client will:

1. look for a Cloudflare KV namespace titled `prod:app`
2. create it if it does not exist
3. cache the resolved namespace for repeated calls

### Tuple Keys

All row operations use tuple keys.

Examples:

- `["users", "user_1"]`
- `["sessions", "session_123"]`

Rules:

- the first segment must be a table name from your schema
- key parts may contain only letters, numbers, and underscores
- characters like `-`, `/`, `:`, and spaces are rejected before any API request is sent

## Quick Start

```ts
import { CfKV } from "cfkv";

type AppSchema = {
	users: { name: string; age: number };
};

const cfkv = new CfKV({
	accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
	apiToken: process.env.CLOUDFLARE_API_TOKEN!,
});

const kv = await cfkv.namespace<AppSchema>("app", "prod");

await kv.put(["users", "user_1"], { name: "Ada", age: 31 });

const user = await kv.get(["users", "user_1"]);
console.log(user);

await kv.delete(["users", "user_1"]);
```

## Usage

### Write One Value

```ts
await kv.put(["users", "user_1"], { name: "Ada", age: 31 });
```

### Write Many Values

```ts
await kv.putBatch([
	{ key: ["users", "user_1"], value: { name: "Ada", age: 31 } },
	{ key: ["users", "user_2"], value: { name: "Bob", age: 28 } },
]);
```

### Read One Value

`get()` returns `undefined` when the key does not exist.

```ts
const user = await kv.get(["users", "user_1"]);
```

### Read Many Values

`getBatch()` preserves the order of the requested keys.

```ts
const [user1, user2, missing] = await kv.getBatch([
	["users", "user_1"],
	["users", "user_2"],
	["users", "missing_user"],
] as const);
```

### Delete One Value

```ts
await kv.delete(["users", "user_1"]);
```

## Listing and Iteration

Listing is table-based, so these methods use only the first tuple segment:

```ts
await kv.fetch(["users"]);
```

That call lists all keys with the prefix `prod:users:` and returns the decoded JSON values.

### Fetch All Rows

`fetch(["users"])` is the convenience form. It follows Cloudflare cursors internally and returns every matching row.

```ts
const users = await kv.fetch(["users"]);
```

Use this when you want all rows in memory.

### Fetch One Page

Use `paginate: true` to get one page plus the next cursor:

```ts
const page1 = await kv.fetch(["users"], {
	paginate: true,
	limit: 50,
});

console.log(page1.items);
console.log(page1.cursor);
```

Fetch the next page by passing the returned cursor back in:

```ts
const page2 = page1.cursor
	? await kv.fetch(["users"], {
			paginate: true,
			limit: 50,
			cursor: page1.cursor,
		})
	: { items: [], cursor: undefined };
```

If `cursor` is `undefined`, there are no more pages.

### Iterate Rows

Use `iterate(["users"])` to stream rows lazily:

```ts
for await (const user of kv.iterate(["users"])) {
	console.log(user);
}
```

### Iterate Pages

Use `iterate(..., { paginate: true })` to stream page objects instead of rows:

```ts
for await (const page of kv.iterate(["users"], { paginate: true, limit: 50 })) {
	console.log(page.items, page.cursor);
}
```

## Pagination Behavior

Cloudflare's key-list endpoint requires paginated `limit` values of at least `10`.

This library enforces that before making a network call:

```ts
await kv.fetch(["users"], { paginate: true, limit: 10 });
```

These are valid:

- `fetch(["users"])`
- `fetch(["users"], { paginate: true, limit: 10 })`
- `iterate(["users"])`
- `iterate(["users"], { paginate: true, limit: 50 })`

This throws:

```ts
await kv.fetch(["users"], { paginate: true, limit: 1 });
```

## Error Handling

When Cloudflare returns a failed response or failed envelope, the client throws `CfKVError`.

```ts
import { CfKVError } from "cfkv";

try {
	await kv.put(["users", "user_1"], { name: "Ada", age: 31 });
} catch (error) {
	if (error instanceof CfKVError) {
		console.error(error.status);
		console.error(error.message);
		console.error(error.errors);
	}
}
```

## Notes and Constraints

- values are stored as JSON
- row keys are serialized as `dbName:table:keyPart:keyPart...`
- `fetch()` and `iterate()` operate on one table prefix at a time
- namespace resolution is cached per `dbName:namespaceName`
- the client uses the global `fetch` by default

## Development

### Verify Types

```bash
bun run verify
```

### Run Tests

```bash
bun test
```

### Build

```bash
bun run build
```

This emits publishable ESM output and type declarations into `dist/`.

## End-to-End Test

The repo includes a live Cloudflare API test.

Environment variables:

- `CLOUDFLARE_ACCOUNT_ID`
- `CLOUDFLARE_API_TOKEN`
- optional `CFKV_E2E_NAMESPACE`
- optional `CFKV_E2E_DB`

Run it with:

```bash
bun test:e2e
```