> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# KV store adapters

> Plug any key-value backend into createFlow via the KvStore interface.

<Note>
  **Open source.** The KV store interface works without an API key. The hosted `WaniwaniKvStore` requires the [Waniwani Platform](/sdk/platform/overview).
</Note>

A flow needs a place to persist its state between MCP calls. MCP tools are stateless, so without a store the engine has nothing to resume from on the next `continue` call. **Every compiled flow is therefore required to have a store.** `.compile()` throws at compile time if you pass neither a `store` argument nor configure `WANIWANI_API_KEY`.

`createFlow` persists this state through a tiny `KvStore` interface. Implement it against any key-value backend: Redis, Upstash, Cloudflare KV, DynamoDB, even a SQLite table. The SDK ships two implementations out of the box, and you can write your own in \~10 lines.

<Note>
  The KV store powers the flow engine only. It is **not** what drives dashboards, funnel analytics, or event tracking. Those are a separate pipeline fed by `withWaniwani(server)` and `client.track()`. You can run a fully self-hosted KV (no API key, nothing leaves your infra) and still get hosted dashboards by also calling `withWaniwani(server)` with `WANIWANI_API_KEY` set. The two paths are independent.
</Note>

## The interface

```ts theme={null}
export interface KvStore<T = Record<string, unknown>> {
  get(key: string): Promise<T | null>;
  set(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
}
```

That's it. The engine handles serialization, session-key derivation, expiry semantics, and concurrency.

## Built-in implementations

The SDK ships two implementations. Pick one or write your own (recipes below).

```ts theme={null}
import { MemoryKvStore, WaniwaniKvStore } from "@waniwani/sdk/mcp";

// In-memory Map, resets on restart. For development and tests.
const flow = createFlow({ /* ... */ }).compile({ store: new MemoryKvStore() });

// Hosted on app.waniwani.ai. Auto-selected when WANIWANI_API_KEY is set
// and no explicit store is passed.
const flow = createFlow({ /* ... */ }).compile(); // picks WaniwaniKvStore

// Or instantiate it manually.
const flow = createFlow({ /* ... */ }).compile({ store: new WaniwaniKvStore() });
```

See the [Platform overview](/sdk/platform/overview) for when the hosted store makes sense versus a self-hosted adapter.

## Adapter recipes

Drop-in `KvStore` implementations for common backends.

### Upstash Redis (serverless, HTTP)

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import { Redis } from "@upstash/redis";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

export const upstashStore: KvStore = {
  async get(key) {
    return (await redis.get(key)) as never;
  },
  async set(key, value) {
    await redis.set(key, value);
  },
  async delete(key) {
    await redis.del(key);
  },
};
```

### Node Redis (`ioredis` or `redis`)

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

export const redisStore: KvStore = {
  async get(key) {
    const raw = await redis.get(key);
    return raw ? JSON.parse(raw) : null;
  },
  async set(key, value) {
    await redis.set(key, JSON.stringify(value));
  },
  async delete(key) {
    await redis.del(key);
  },
};
```

### Cloudflare Workers KV

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";

export function cloudflareKvStore(ns: KVNamespace): KvStore {
  return {
    async get(key) {
      return ns.get<Record<string, unknown>>(key, "json");
    },
    async set(key, value) {
      await ns.put(key, JSON.stringify(value));
    },
    async delete(key) {
      await ns.delete(key);
    },
  };
}

// In your worker:
const flow = createFlow({ /* ... */ }).compile({
  store: cloudflareKvStore(env.FLOW_STATE_KV),
});
```

### DynamoDB

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  DeleteCommand,
  GetCommand,
  PutCommand,
} from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = "flow_state";

export const dynamoStore: KvStore = {
  async get(key) {
    const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk: key } }));
    return (res.Item?.value as never) ?? null;
  },
  async set(key, value) {
    await ddb.send(new PutCommand({ TableName: TABLE, Item: { pk: key, value } }));
  },
  async delete(key) {
    await ddb.send(new DeleteCommand({ TableName: TABLE, Key: { pk: key } }));
  },
};
```

### SQLite (better-sqlite3)

Quick local persistence without standing up a separate service.

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import Database from "better-sqlite3";

const db = new Database("flow-state.db");
db.exec("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)");

const getStmt = db.prepare("SELECT value FROM kv WHERE key = ?");
const setStmt = db.prepare("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)");
const delStmt = db.prepare("DELETE FROM kv WHERE key = ?");

export const sqliteStore: KvStore = {
  async get(key) {
    const row = getStmt.get(key) as { value: string } | undefined;
    return row ? JSON.parse(row.value) : null;
  },
  async set(key, value) {
    setStmt.run(key, JSON.stringify(value));
  },
  async delete(key) {
    delStmt.run(key);
  },
};
```

## Encryption at rest

`WaniwaniKvStore` encrypts values at rest with AES-256-GCM before they leave the SDK. You don't have to do anything; it's handled inside the store.

For self-hosted stores, encryption is your responsibility. If your backend doesn't provide encryption at rest, wrap your adapter:

```ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

function encrypted(inner: KvStore, key: Buffer): KvStore {
  return {
    async get(k) {
      const raw = await inner.get(k);
      if (!raw) return null;
      // ...decrypt with key, return parsed value
    },
    async set(k, value) {
      // ...encrypt JSON.stringify(value) with key
      await inner.set(k, /* encrypted envelope */);
    },
    async delete(k) {
      await inner.delete(k);
    },
  };
}
```

The `KvStore` interface composes trivially. Encrypt at the wrapper boundary; the engine stays unaware.

## What gets stored

Each session maps to one key. The engine stores:

* The current node (or `END` when complete)
* The merged state object so far
* A pending widget reference, if the current node is a `showWidget` step

Keys are derived from the MCP session identifier (`_meta.waniwani/sessionId` or `Mcp-Session-Id`), so isolating sessions is automatic. Values are JSON-serializable plain objects.

## Choosing a backend

| Goal                                 | Pick                         |
| ------------------------------------ | ---------------------------- |
| Local dev, tests, ephemeral          | `MemoryKvStore`              |
| Zero infra, encrypted at rest        | `WaniwaniKvStore` (Platform) |
| Vercel / Netlify / serverless edge   | Upstash Redis adapter        |
| Cloudflare Workers                   | Workers KV adapter           |
| AWS Lambda                           | DynamoDB adapter             |
| Single-node Node.js with persistence | SQLite or Node Redis         |

Picking `WaniwaniKvStore` does not by itself enable dashboards or funnel analytics. Those come from tracking events emitted by `withWaniwani(server)`. The two systems are independent.
