npm package: @skroyc/langgraph-supabase-checkpointer
Description: LangGraphJS checkpoint saver implementation for Supabase with UUID thread_id and Supabase Storage support
Author: SkrOYC
Latest version: 2.2.1

<div align="center">

# @skroyc/langgraph-supabase-checkpointer

[![npm version](https://img.shields.io/npm/v/@skroyc/langgraph-supabase-checkpointer)](https://www.npmjs.com/package/@skroyc/langgraph-supabase-checkpointer)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/)

A robust, production-ready LangGraphJS checkpoint saver implementation for Supabase. **Version 2.1 adds support for the `deleteThread` lifecycle method, alongside existing features like UUID thread IDs and optimized Supabase Storage integration.**

[**Database Setup**](#-database-setup) •
[**Quick Start**](#-quick-start) •
[**API Reference**](#-api-reference) •
[**Contributing**](#-contributing)

</div>

## 📋 Table of Contents

- [Features](#-features)
- [Installation](#-installation)
- [Database Setup](#-database-setup)

- [Quick Start](#-quick-start)
- [Documentation](#-documentation)
  - [API Reference](#-api-reference)
  - [Authentication Modes](#-authentication-modes)
  - [Advanced Configuration](#-advanced-configuration)
- [Examples](#-examples)
  - [Basic Chatbot with Memory](#basic-chatbot-with-memory)
  - [Supabase Edge Function Integration](#supabase-edge-function-integration)
- [Architecture](#-architecture)
- [Performance Considerations](#-performance-considerations)
- [Troubleshooting](#-troubleshooting)
- [Contributing](#-contributing)
- [Acknowledgements](#-acknowledgements)
- [License](#-license)

## ✨ Features

- **🚀 Full LangGraphJS Compatibility**: Implements the complete `BaseCheckpointSaver` interface.
- **🗑️ Thread Deletion**: Includes the `deleteThread` method for permanently removing all data associated with a thread.
- **🔒 Secure & Multi-Tenant**: Isolates data by `user_id` with Row Level Security (RLS) enabled by default.
- **⚡ High Performance**: Optimized for high-throughput applications with atomic PostgreSQL functions and efficient blob storage.
- **🧩 TypeScript First**: Full type safety and excellent IDE support.
- **✅ Officially Validated**: Passes the entire official LangGraphJS checkpoint validation test suite (710/710 tests).
- **📦 Minimal Dependencies**: Only requires peer dependencies for core functionality.
- **🆔 UUID Thread IDs**: V2 uses UUID format for thread identifiers providing better performance and scalability.
- **💾 Supabase Storage Integration**: Large blob data is stored in Supabase Storage for optimal performance and cost efficiency.

## 🚀 Installation

### Prerequisites

- Node.js 18+ or Bun 1.0+
- A Supabase project with PostgreSQL 14+
- Basic understanding of LangGraphJS and its checkpointing system.

### 1. Install Package

Install the package using your preferred package manager:

```bash
# Using npm
npm install @skroyc/langgraph-supabase-checkpointer

# Using Yarn
yarn add @skroyc/langgraph-supabase-checkpointer

# Using pnpm
pnpm add @skroyc/langgraph-supabase-checkpointer

# Using Bun
bun add @skroyc/langgraph-supabase-checkpointer
```

### 2. Install Peer Dependencies

This package requires the following peer dependencies:

```bash
# Using npm
npm install @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-js

# Using Yarn
yarn add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-js

# Using pnpm
pnpm add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-js

# Using Bun
bun add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-js
```

### 3. Set Environment Variables

Create a `.env` file in your project root with the following variables:

```env
# Required
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key

# Optional: Only needed for admin operations
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
```

## 🗄️ Database Setup

To set up your database, you need to run the `migrations.sql` script included with this package. This script is idempotent, meaning it can be run multiple times without causing errors.

There are two recommended ways to run the migration:

#### Option 1: Supabase Dashboard

1.  Go to your [Supabase Dashboard](https://app.supabase.com) and select your project.
2.  Navigate to the **SQL Editor**.
3.  Open the `migrations.sql` file from this package, copy its contents, and paste them into a new query.
4.  Click **Run**.

#### Option 2: Supabase CLI (or `psql`)

If you use the Supabase CLI or have direct `psql` access to your database, you can run the file directly.

**Using Supabase CLI:**

1.  Create a new migration file in your Supabase project (e.g., `supabase/migrations/<timestamp>_langgraph_checkpointer.sql`).
2.  Copy the content of `migrations.sql` from this package into the new file.
3.  Run `supabase db push` to apply the migration.

**Using `psql`:**

```bash
# Ensure you have the migrations.sql file
psql -h your-db-host -U postgres -d postgres -f migrations.sql
```

### Supabase Storage Configuration

The `migrations.sql` script automatically attempts to create the required `checkpoints` storage bucket and configure its security policies.

**Note:** The user running the migration needs storage admin privileges to perform these actions. If the script fails due to insufficient permissions, you will need to create the bucket and policy manually as described below.

<details>
<summary>Click for manual Storage setup instructions</summary>

1.  **Create Storage Bucket**: In your Supabase Dashboard, navigate to **Storage** and create a new bucket named `checkpoints`.
2.  **Configure Bucket Policy**: In the **SQL Editor**, run the following query to create the Row Level Security policy for the bucket. This ensures users can only access their own data.

    ```sql
    -- Storage bucket policy for checkpoints
    CREATE POLICY "Users can access their own checkpoint blobs" ON storage.objects
      FOR ALL USING (bucket_id = 'checkpoints' AND auth.uid()::text = (storage.foldername(name))[1]);
    ```

</details>



## ⚡ Quick Start

This example demonstrates how to create a simple chatbot that remembers conversation history using the `SupabaseSaver`.

```typescript
import { SupabaseSaver } from '@skroyc/langgraph-supabase-checkpointer';
import { createClient } from '@supabase/supabase-js';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { v4 as uuidv4 } from 'uuid';

// 1. Initialize Supabase client
const supabaseClient = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
  {
    auth: { persistSession: false } // Recommended for server-side usage
  }
);

// 2. Create a checkpoint saver instance (for a specific user)
const checkpointer = new SupabaseSaver(supabaseClient, undefined, 'user-123');

// 3. Create a LangGraph agent with persistence enabled
const agent = await createReactAgent({
  tools: [], // Your tools here
  llm: new ChatGoogleGenerativeAI({
    model: 'gemini-2.5-flash',
    apiKey: process.env.GOOGLE_API_KEY!,
    temperature: 0.7,
  }),
  checkpointSaver: checkpointer,
});

// 4. Use the agent with a persistent thread_id
async function chatWithMemory(threadId: string, message: string) {
  const result = await agent.invoke(
    { messages: [{ role: "user", content: message }] },
    {
      configurable: {
        thread_id: threadId,
        metadata: {
          app_version: '1.0.0',
          environment: process.env.NODE_ENV!,
        },
      }
    }
  );

  // The final message is usually the last one in the array
  const lastMessage = result.messages[result.messages.length - 1];
  return lastMessage.content;
}

// --- Run the example ---
async function runExample() {
  const threadId = uuidv4(); // V2: Generate UUID for thread ID

  // First message
  console.log('You: Hello, my name is Alice');
  const response1 = await chatWithMemory(threadId, 'Hello, my name is Alice');
  console.log('AI:', response1);

  // Second message - the AI should remember the context from the Supabase checkpointer
  console.log('\nYou: What is my name?');
  const response2 = await chatWithMemory(threadId, 'What is my name?');
  console.log('AI:', response2);
}

runExample().catch(console.error);
```

### Expected Output

```
You: Hello, my name is Alice
AI: Hello Alice! How can I assist you today?

You: What is my name?
AI: Your name is Alice! How can I help you today, Alice?
```

## 📚 Documentation

### 🔍 API Reference

#### `SupabaseSaver` Class

The main class that implements the `BaseCheckpointSaver` interface from LangGraph.

##### `new SupabaseSaver(client, serde?, userId?)`

**Constructor Parameters:**
- `client: SupabaseClient`: The Supabase client instance from `@supabase/supabase-js`.
- `serde?: SerializerProtocol`: (Optional) A custom serialization protocol. Defaults to LangGraph's `JsonPlusSerializer`.
- `userId?: string`: (Optional) The user ID for server-side usage. If not provided, the checkpointer will attempt to retrieve it from the client's auth session.

#### Core Methods

All methods implement the `BaseCheckpointSaver` interface.

##### `getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>`
Retrieves a checkpoint tuple for a given configuration.
```typescript
const checkpoint = await checkpointer.getTuple({
  configurable: {
    thread_id: "my-thread",
    // Optional: Specify a specific checkpoint ID
    checkpoint_id: "specific-checkpoint-id"
  }
});
```

##### `put(config: RunnableConfig, checkpoint: Checkpoint, metadata?: Record<string, unknown>): Promise<RunnableConfig>`
Stores a checkpoint with its metadata atomically.

##### `putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>`
Stores pending writes for a checkpoint.

##### `list(config: RunnableConfig, options?: ListOptions): AsyncGenerator<CheckpointTuple>`
Lists checkpoints matching a configuration, with support for filtering and pagination.

##### `deleteThread(threadId: string): Promise<void>`
Permanently deletes a thread and all its associated data, including checkpoints and stored blobs.

### 🔒 Authentication Modes

#### Server-side (Recommended)
When using the checkpointer in a secure backend environment like a Supabase Edge Function or a Node.js server, pass the `userId` directly to the constructor. This is the most secure and reliable method.

```typescript
// Pass the user's ID directly for data isolation
const checkpointer = new SupabaseSaver(supabaseClient, undefined, user.id);
```

#### Client-side
If used on the client-side (e.g., in a browser), the checkpointer will rely on the active Supabase auth session to get the user ID. Ensure the user is authenticated.

```typescript
// Will automatically get user ID from the active auth session
const checkpointer = new SupabaseSaver(supabaseClient);
```

### ⚙️ Advanced Configuration

You can control thread management through the `configurable` object.

```typescript
const config = {
  configurable: {
    // Required: The ID for the conversation thread (V2: must be UUID)
    thread_id: uuidv4(), // or existing UUID string

    // Optional: A namespace to further segment checkpoints within a thread
    checkpoint_ns: "optional-namespace", // Default: ""

    // Optional: A specific checkpoint ID to get or resume from
    checkpoint_id: "specific-checkpoint-id"
  }
};
```

## 💡 Examples

### Basic Chatbot with Memory

This snippet shows how two separate invocations on the same `thread_id` maintain context.

```typescript
import { SupabaseSaver } from '@skroyc/langgraph-supabase-checkpointer';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { HumanMessage } from '@langchain/core/messages';
import { v4 as uuidv4 } from 'uuid';

const checkpointer = new SupabaseSaver(supabaseClient, undefined, 'user-123');
const agent = createReactAgent({ tools: [], llm, checkpointSaver: checkpointer });

// V2: Use UUID for thread ID
const threadId = uuidv4();

// First invocation
await agent.invoke(
  { messages: [new HumanMessage("My name is Alice")] },
  { configurable: { thread_id: threadId } }
);

// Later invocation - the agent will remember the name "Alice"
const response = await agent.invoke(
  { messages: [new HumanMessage("What's my name?")] },
  { configurable: { thread_id: threadId } }
);
```

### Supabase Edge Function Integration

Here is how you can securely use the checkpointer within a Supabase Edge Function.

```typescript
// In your Supabase Edge Function (e.g., supabase/functions/chat/index.ts)
import { createClient } from '@supabase/supabase-js';
import { SupabaseSaver } from '@skroyc/langgraph-supabase-checkpointer';

const supabaseClient = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!);

Deno.serve(async (req) => {
  // Securely get the user from the Authorization header
  const { data: { user } } = await supabaseClient.auth.getUser(
    req.headers.get('Authorization')?.replace('Bearer ', '') ?? ''
  );

  if (!user) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Instantiate the checkpointer with the authenticated user's ID
  const checkpointer = new SupabaseSaver(supabaseClient, undefined, user.id);

  // ... now use the checkpointer with your LangGraph agent
  // const agent = createReactAgent({ ..., checkpointSaver: checkpointer });
  // const result = await agent.invoke(...);

  return new Response(JSON.stringify({ message: "Success" }), {
    headers: { 'Content-Type': 'application/json' },
  });
});
```

## 🏗️ Architecture

### Database Schema

The schema is designed for performance and scalability by separating checkpoint metadata from large binary data, which is offloaded to Supabase Storage.

-   **`checkpoints`**: Stores the core checkpoint data, metadata, and parent references.
-   **`checkpoint_blobs`**: Stores `TEXT` paths that point to the actual blob data in Supabase Storage. This keeps the database table small and lookups fast.
-   **`checkpoint_writes`**: Stores pending writes as `bytea` before they are consolidated into a checkpoint.

### Key Design Decisions

1.  **Supabase Storage for Blobs**: Large binary data (channel values) is uploaded to Supabase Storage. The database only stores the path to the object, which is more efficient for database performance and allows for leveraging Supabase's dedicated file storage infrastructure.
2.  **Atomic RPC Functions**: Using PostgreSQL functions (`put_checkpoint_and_blobs`) ensures that checkpoints and their associated blob metadata are written in a single, atomic transaction, preventing data inconsistency.
3.  **User-based Isolation**: Row Level Security (RLS) policies are enabled by default to ensure users can only access their own data, both in the database and in the storage bucket.
4.  **Efficient Serialization**: Uses LangGraph's default `JsonPlusSerializer` for broad compatibility.

## 🔍 Performance Considerations

The implementation includes several database optimizations:

-   **Efficient Indexing**: Indexes are created on `thread_id`, `user_id`, and `created_at` to accelerate common query patterns like fetching the latest checkpoint or listing a thread's history.
-   **Atomic Operations**: PostgreSQL functions reduce network round-trips and ensure data consistency.
-   **Row Level Security**: RLS is highly performant in PostgreSQL and provides robust, built-in security without application-level overhead.

## 🛠️ Troubleshooting

#### "Function not found" errors
This almost always means the database migration script was not run or did not complete successfully. Please re-run the SQL script from the [Database Setup](#-database-setup) section.

#### "User not authenticated" or RLS violation errors
Ensure the user is properly authenticated when using client-side mode. For server-side usage, verify that you are correctly passing a valid `userId` to the `SupabaseSaver` constructor.

#### Debugging with SQL
You can inspect the database tables directly to debug issues:
```sql
-- View the 10 most recent checkpoints
SELECT thread_id, checkpoint_id, metadata, created_at FROM public.checkpoints ORDER BY created_at DESC LIMIT 10;

-- View recent blob storage paths
SELECT thread_id, channel, value as storage_path, type FROM public.checkpoint_blobs ORDER BY created_at DESC LIMIT 10;

-- View pending writes
SELECT thread_id, checkpoint_id, task_id, channel FROM public.checkpoint_writes ORDER BY created_at DESC LIMIT 10;
```

## 🙏 Acknowledgements

-   [LangChain](https://langchain.com/) for the amazing LangGraph framework.
-   [Supabase](https://supabase.com/) for their incredible developer platform.

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.