Implementing Custom Session Stores for Persistent Conversation State in Flue

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, 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#L539-L543):

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:

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:

// 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:

// 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:

// 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#L93-L107), the init() function accepts a store property in its configuration object:

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

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). 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), 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), 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →