Skip to main content
The Supabase backend persists MCP sessions in a mcp_sessions PostgreSQL table with row-level security policies, automatic expires_at management, and application-level AES-256-GCM encryption for sensitive fields. It is a strong choice for teams already on Supabase and for Next.js applications that benefit from Supabase’s built-in auth integration.

Install the peer dependency

npm install @supabase/supabase-js

Set up the database

Before the backend can start, the mcp_sessions table must exist in your Supabase project. Use the mcp-ts CLI to eject the required migration into your project’s supabase/migrations/ folder.
1

Eject the migration files

Run the following command from the root of your project:
npx mcp-ts supabase-init
This copies the bundled SQL migration file into ./supabase/migrations/. Existing files are not overwritten.
2

Link your Supabase project

npx supabase link --project-ref your-project-id
Find your project ID in the Supabase dashboard URL: https://supabase.com/dashboard/project/<your-project-id>.
3

Push the migration

npx supabase db push
This creates the mcp_sessions table and configures the RLS policies.
4

Set environment variables

Add the following to your .env file:
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Find the service role key in Supabase Dashboard → Project Settings → API → service_role.
Always use SUPABASE_SERVICE_ROLE_KEY — never SUPABASE_ANON_KEY — for server-side storage. The anon key is subject to Row Level Security policies that will block session writes and return permission errors. The service role key is designed for trusted server-to-server operations and bypasses RLS. If the SDK detects you are using the anon key, it prints a warning at startup but continues; you will likely see errors on the first write.

Configure environment variables

# Force Supabase selection (optional — auto-detected if SUPABASE_URL + key are present)
MCP_TS_STORAGE_TYPE=supabase

# Required
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

How auto-detection works

If MCP_TS_STORAGE_TYPE is not set, the SDK detects Supabase when both SUPABASE_URL and either SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY are present. Supabase is checked after Redis and File but before the Memory fallback. If the Supabase connection or table check fails at startup, the SDK logs an error and falls back to in-memory storage.

Encryption at rest

The Supabase backend automatically encrypts the tokens and headers fields using AES-256-GCM before writing to the database. Your Supabase project only ever stores cipher text for those fields; decryption happens in your Node.js process. To enable encryption, generate a 32-byte key and set it as an environment variable:
# Generate a key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# Add to your .env
STORAGE_ENCRYPTION_KEY=your-64-character-hex-string
Encrypted values in the database look like this:
{
  "tokens": "enc:1:cd4511ef932b...:3f2a1b...:a4b5c6d7...",
  "headers": "enc:1:1234abcd...:..."
}
If STORAGE_ENCRYPTION_KEY is not set, the SDK prints a single startup warning and saves data unencrypted. You can enable encryption at any time; existing unencrypted sessions remain readable, and new sessions will be encrypted going forward.
Never commit STORAGE_ENCRYPTION_KEY to version control. If the key is lost, encrypted session data cannot be recovered. Rotate it the same way you would rotate a database password.

Example: use the auto-detected storage export

import { storage } from '@mcp-ts/sdk/server';

// With SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY in your environment,
// `storage` automatically uses the Supabase backend.
const sessionId = await storage.generateSessionId();

await storage.createSession({
  sessionId,
  identity: 'user-456',
  serverUrl: 'https://mcp.example.com',
  callbackUrl: 'https://app.example.com/callback',
  transportType: 'sse',
  createdAt: Date.now(),
});

const sessions = await storage.getIdentitySessionsData('user-456');

Example: instantiate directly with an existing client

Use this pattern when your application already constructs a Supabase client and you want to reuse it:
import { createSupabaseStorageBackend } from '@mcp-ts/sdk/server';
import { createClient } from '@supabase/supabase-js';

// Always use the service_role key on the server
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const storage = createSupabaseStorageBackend(supabase);
await storage.init(); // verifies the mcp_sessions table exists; throws if missing

Startup validation

When init() runs, the backend queries mcp_sessions with a zero-row SELECT to confirm the table exists. If the table is missing, it throws a descriptive error:
[SupabaseStorage] Table "mcp_sessions" not found in your database.
Please run "npx mcp-ts supabase-init" in your project to set up the required table and RLS policies.
If the table exists, you will see:
[mcp-ts][Storage] Supabase: ✓ "mcp_sessions" table verified.

Troubleshooting

permission denied for table mcp_sessions — You are using the anon key. Switch to SUPABASE_SERVICE_ROLE_KEY. Table "mcp_sessions" not found — The migration has not been applied. Run npx mcp-ts supabase-init then npx supabase db push. Failed to initialize Supabase: ... — Check that SUPABASE_URL is a valid https:// URL and that the key matches the project.