LangGraphJS checkpoint saver implementation for Supabase with UUID thread_id and Supabase Storage support
npm install @skroyc/langgraph-supabase-checkpointer


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.
- Features
- Installation
- Database Setup
- Quick Start
- Documentation
- API Reference
- Authentication Modes
- Advanced Configuration
- Examples
- Basic Chatbot with Memory
- Supabase Edge Function Integration
- Architecture
- Performance Considerations
- Troubleshooting
- Contributing
- Acknowledgements
- License
- 🚀 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.
- Node.js 18+ or Bun 1.0+
- A Supabase project with PostgreSQL 14+
- Basic understanding of LangGraphJS and its checkpointing system.
Install the package using your preferred package manager:
``bashUsing npm
npm install @skroyc/langgraph-supabase-checkpointer
$3
This package requires the following peer dependencies:
`bash
Using npm
npm install @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-jsUsing Yarn
yarn add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-jsUsing pnpm
pnpm add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-jsUsing Bun
bun add @langchain/langgraph-checkpoint @langchain/core @supabase/supabase-js
`$3
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_keyOptional: 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 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/).
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
`$3
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.
Click for manual Storage setup instructions
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]);
`⚡ 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);
`$3
`
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
$3
####
SupabaseSaver ClassThe 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
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
Stores a checkpoint with its metadata atomically.#####
putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise
Stores pending writes for a checkpoint.#####
list(config: RunnableConfig, options?: ListOptions): AsyncGenerator
Lists checkpoints matching a configuration, with support for filtering and pagination.#####
deleteThread(threadId: string): Promise
Permanently deletes a thread and all its associated data, including checkpoints and stored blobs.$3
#### 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);
`$3
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
$3
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 } }
);
`$3
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
$3
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.$3
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 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;
``- LangChain for the amazing LangGraph framework.
- Supabase for their incredible developer platform.
This project is licensed under the MIT License - see the LICENSE file for details.