# Implementing Custom Session Stores for Persistent Conversation State in Flue

> Learn to implement custom session stores in Flue to persist conversation state across server restarts. Override save load and delete methods with the SessionStore interface.

- Repository: [Astro/flue](https://github.com/withastro/flue)
- Tags: how-to-guide
- Published: 2026-05-11

---

**You can persist Flue conversation state across server restarts by implementing the `SessionStore` interface with three async methods—`save()`, `load()`, and `delete()`—and passing your custom store to the `init()` function.**

Flue, Astro’s AI SDK for building conversational agents, abstracts session persistence behind a minimal interface that lets you swap storage backends without changing agent logic. By implementing the `SessionStore` interface defined in [`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts), you can persist conversation trees to PostgreSQL, Redis, SQLite, or any external database while the core SDK automatically handles serialization and lifecycle management.

## Understanding the SessionStore Interface

Flue’s runtime tracks every conversation in a **session** that stores a tree of messages, compactions, and branch summaries inside a `SessionData` object. Persistence is abstracted behind the `SessionStore` interface located at [[`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts#L539-L543):

```typescript
export interface SessionStore {
  /** Write a session snapshot. */
  save(id: string, data: SessionData): Promise<void>;

  /** Load a snapshot, or `null` if it does not exist. */
  load(id: string): Promise<SessionData | null>;

  /** Delete a snapshot. */
  delete(id: string): Promise<void>;
}

```

Any object that implements these three async methods qualifies as a valid store. The `SessionData` type (defined at lines 493-511 in the same file) contains the conversation transcript, but your implementation decides where that JSON-serializable payload lives.

## Built-in Storage Options

Flue ships with two reference implementations:

- **In-memory store**: Defined at [[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts#L558-L564), this keeps data only for the lifetime of the Node process. It is useful for development but loses state on restart.
- **Cloudflare DO store**: Located at [[`packages/sdk/src/cloudflare/session-store.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts#L8-L27), this persists data in Cloudflare Durable Objects state (SQLite-backed by default).

When you need durability across process restarts, shared state between multiple agents, or integration with existing backend infrastructure, you replace these with a custom implementation.

## Creating a Custom SessionStore Implementation

### SQLite Session Store Example

The following implementation uses `better-sqlite3` to persist sessions to a local database file. It handles JSON serialization and uses an upsert pattern to update existing records:

```typescript
// my-sqlite-store.ts
import type { SessionData, SessionStore } from '@flue/sdk';
import Database from 'better-sqlite3';

const db = new Database('flue.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    data TEXT NOT NULL,
    updated_at TEXT NOT NULL
  );
`);

export const sqliteStore: SessionStore = {
  async save(id: string, data: SessionData) {
    const now = new Date().toISOString();
    const json = JSON.stringify(data);
    db.prepare(`
      INSERT INTO sessions (id, data, updated_at) VALUES (?, ?, ?)
      ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at;
    `).run(id, json, now);
  },

  async load(id: string) {
    const row = db.prepare(`SELECT data FROM sessions WHERE id = ?`).get(id) as { data: string } | undefined;
    return row ? JSON.parse(row.data) as SessionData : null;
  },

  async delete(id: string) {
    db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
  },
};

```

All three methods return `Promise<void>` or `Promise<SessionData | null>` to stay consistent with the SDK’s async contract.

### File-Based JSON Store

For simpler use cases, you can persist sessions as individual JSON files on disk:

```typescript
// file-store.ts
import { SessionData, SessionStore } from '@flue/sdk';
import { promises as fs } from 'fs';
import path from 'path';

const dir = path.resolve('.flue-sessions');
await fs.mkdir(dir, { recursive: true });

export const fileStore: SessionStore = {
  async save(id: string, data: SessionData) {
    const file = path.join(dir, `${id}.json`);
    await fs.writeFile(file, JSON.stringify(data));
  },

  async load(id: string) {
    const file = path.join(dir, `${id}.json`);
    try {
      const raw = await fs.readFile(file, 'utf-8');
      return JSON.parse(raw) as SessionData;
    } catch {
      return null;
    }
  },

  async delete(id: string) {
    const file = path.join(dir, `${id}.json`);
    await fs.unlink(file).catch(() => {});
  },
};

```

### Redis Store Implementation

For distributed deployments, implement the interface using a Redis client:

```typescript
// redis-store.ts
import { SessionData, SessionStore } from '@flue/sdk';
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

export const redisStore: SessionStore = {
  async save(id: string, data: SessionData) {
    await client.set(id, JSON.stringify(data));
  },

  async load(id: string) {
    const raw = await client.get(id);
    return raw ? (JSON.parse(raw) as SessionData) : null;
  },

  async delete(id: string) {
    await client.del(id);
  },
};

```

## Wiring Your Store into Flue

Pass your custom store via the `SessionInitOptions` structure when initializing a session. According to the source code at [[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts#L93-L107), the `init()` function accepts a `store` property in its configuration object:

```typescript
import { init } from '@flue/sdk';
import { sqliteStore } from './my-sqlite-store';

const { session } = await init({
  name: 'my-agent',
  // ... other SDK config such as roles, systemPrompt, etc.
  store: sqliteStore,  // <-- inject your store here
});

```

Once injected, every call to `session.prompt()`, `session.task()`, and `session.shell()` automatically reads from and writes through your store. After a prompt completes, the SDK invokes `await this.save()` inside the `Session` class, which ultimately calls your implementation’s `save()` method.

## Key Source Files for Reference

- **[[`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)**: Contains the `SessionStore` interface definition and the `SessionData` type shape.
- **[[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)**: Houses the core `Session` class, the constructor that receives the `store`, and the internal `save()` method that triggers persistence.
- **[[`packages/sdk/src/cloudflare/session-store.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts)**: Reference implementation for platform-specific storage using Cloudflare Durable Objects.
- **[[`packages/sdk/src/usage.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/usage.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/usage.ts)**: Helpers for aggregating token usage metrics; useful if you want to store cost data alongside conversation transcripts.

## Summary

- **Implement `SessionStore`** with `save()`, `load()`, and `delete()` methods to control where conversation data persists.
- **Pass your store** via the `store` property in `SessionInitOptions` when calling `init()`.
- **Automatic persistence**: Flue invokes your store after every `prompt()`, `task()`, compaction, and explicit deletion via `session.delete()`.
- **Environment portability**: The same custom store works in local Node.js development, CI environments, and serverless deployments like Cloudflare Workers without code changes.

## Frequently Asked Questions

### What data structure does Flue store in the session?

Flue stores a **`SessionData`** object containing a tree of messages, compactions, and branch summaries. The exact TypeScript shape is defined at lines 493-511 in [[`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts). Your `SessionStore` implementation receives this object as the `data` parameter in `save()` and must return it from `load()`.

### Can I use the same custom store with Cloudflare Workers?

Yes. While Flue provides a Cloudflare-specific implementation in [[`packages/sdk/src/cloudflare/session-store.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/cloudflare/session-store.ts), you can substitute any custom `SessionStore` that connects to external databases like PlanetScale, Upstash Redis, or DynamoDB. This makes your persistence layer portable across Node.js and edge environments.

### When exactly does Flue call the `save()` method?

The SDK calls `save()` automatically after completing `session.prompt()` and `session.task()` calls, during session compaction, and when explicitly calling `session.delete()`. According to the implementation in [[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts), you do not need to manually trigger persistence during normal conversation flows.

### How do I handle concurrent updates to the same session ID?

Your `SessionStore` implementation must handle concurrency according to your backend’s capabilities. For SQLite, use transactions or UPSERT syntax as shown in the example. For Redis, consider using optimistic locking or atomic operations if multiple processes might write to the same session simultaneously.