Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt

Use this file to discover all available pages before exploring further.

Wrap any Vercel AI SDK model with one line and get persistent, per-user memory on every call. generateText, streamText, and generateObject all keep working — the middleware just makes them memory-aware.
@maximem/synap-vercel-adk wraps the JavaScript SDK, which in turn wraps the Python SDK as a subprocess. It requires a Python 3.11+ runtime on the host. Edge Runtime, Cloudflare Workers, Bun, Deno Deploy, and AWS Lambda Node-only runtimes are not supported. Pin Next.js route handlers to export const runtime = "nodejs". See Installation → JavaScript / TypeScript SDK.

Overview

This guide shows how to add Synap to a Vercel AI SDK application to build apps that:
  • Inject relevant memory before every generateText / streamText / generateObject call
  • Record completed turns back to Synap automatically
  • Work with any provider — OpenAI, Anthropic, Google, etc. — without per-provider plumbing
The Synap Vercel AI SDK integration ships a factory plus a provider class.
ExportPurpose
createSynapAsync factory that initializes the Synap provider
SynapProviderProvider class with wrap and listen methods

Setup

Install the package:
npm install @maximem/synap-vercel-adk ai @ai-sdk/openai
Configure your API key. Generate one from the Synap Dashboard.
.env
SYNAP_API_KEY=synap_your_key_here
OPENAI_API_KEY=your-openai-api-key
Initialize the provider once at application startup:
import { createSynap } from "@maximem/synap-vercel-adk";

const synap = await createSynap({
  apiKey: process.env.SYNAP_API_KEY!,
});
createSynap initializes the underlying Synap SDK internally — you don’t need to manage the SDK lifecycle separately. See SDK Initialization for the full lifecycle if you’d rather construct the SDK directly.

Basic integration

The smallest useful integration wraps a model with synap.wrap and uses it like any other Vercel AI SDK model:
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const model = synap.wrap(anthropic("claude-sonnet-4-6"), {
  userId: "alice",
  customerId: "acme",   // optional — required for B2B instances
});

const { text } = await generateText({
  model,
  messages: [{ role: "user", content: "What do you remember about my account?" }],
});
On every call, the middleware fetches Synap context, injects it as a system message, proxies the request to the wrapped model, and ingests the resulting turn back into Synap. Context-fetch failures degrade gracefully (empty context, error logged); turn-ingestion failures surface explicitly so silent data loss is impossible.

Core concepts

synap.wrap

synap.wrap(model, options) returns a standard Vercel AI SDK LanguageModel. Drop it anywhere you’d use the underlying model — generateText, streamText, generateObject, agentic loops, structured output, etc.
const model = synap.wrap(openai("gpt-4o"), {
  userId: "alice",
  customerId: "acme",
  conversationId: "conv-001",  // optional — biases retrieval to this thread
});
The scoping triple is bound when you call wrap — the model only ever sees the messages, never userId/customerId. This means prompt injection cannot spoof scope.

The middleware loop

On every call to the wrapped model:
your code → synap.wrap(model) → [fetch context] → wrapped model → [ingest turn] → your code
  1. Before — fetches the user’s Synap context and injects it as a system message.
  2. Generates — proxies the request to the wrapped model unchanged.
  3. After — ingests the completed user + assistant turn into Synap asynchronously.
Steps 1 and 3 are independent — a failure in either does not block the other.

Provider-agnostic

Wrap any Vercel AI SDK-compatible model:
import { openai } from "@ai-sdk/openai";
import { google } from "@ai-sdk/google";
import { anthropic } from "@ai-sdk/anthropic";

const gptWithMemory    = synap.wrap(openai("gpt-4o"),                  { userId: "alice" });
const geminiWithMemory = synap.wrap(google("gemini-2.0-flash"),         { userId: "alice" });
const claudeWithMemory = synap.wrap(anthropic("claude-sonnet-4-6"),     { userId: "alice" });
The middleware operates on the Vercel AI SDK abstraction, so the same wrap behavior applies to every provider.

Streaming

streamText and streamObject work without any code changes — the middleware injects context before the stream starts and ingests the turn when the stream ends:
import { streamText } from "ai";

const { textStream } = await streamText({
  model: synap.wrap(openai("gpt-4o"), { userId: "alice" }),
  messages: [{ role: "user", content: "Summarize my recent priorities." }],
});

for await (const chunk of textStream) {
  process.stdout.write(chunk);
}

Complete example: chat route with per-request scoping

The pattern below is a typical Next.js (Node runtime) chat route. Each request gets its own scope baked into a freshly-wrapped model, so multiple concurrent users cannot leak into each other’s memory:
// app/api/chat/route.ts
export const runtime = "nodejs";   // required — see runtime warning above

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createSynap } from "@maximem/synap-vercel-adk";

// Initialize once at module load
const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! });

export async function POST(req: Request) {
  const { userId, customerId, messages } = await req.json();

  const model = synap.wrap(openai("gpt-4o"), { userId, customerId });

  const result = await streamText({ model, messages });
  return result.toDataStreamResponse();
}
Three things to notice in this pattern:
  1. The provider is module-scoped, the model is request-scoped. createSynap runs once; synap.wrap runs per request with the right userId.
  2. Streaming is unchanged. No special integration needed — streamText and the middleware co-exist transparently.
  3. The runtime = "nodejs" pin matters. Edge Runtime would break the Python-subprocess dependency.

Advanced patterns

Per-request scoping

For multi-tenant services, build the wrapped model per request and never cache it across users:
async function handleChat(userId: string, message: string) {
  const model = synap.wrap(openai("gpt-4o"), { userId });
  const { text } = await generateText({
    model,
    messages: [{ role: "user", content: message }],
  });
  return text;
}

Anticipation stream

synap.listen() opens a gRPC stream that pre-fetches context speculatively before the user’s next request arrives. This reduces perceived latency in long-lived server processes where you can predict who will message next:
const stop = synap.listen({ userId: "alice" });

// Later, when the session ends:
stop();
Use this for active sessions where you’ve already identified the user (e.g. after authentication on an open WebSocket) and want to warm up retrieval ahead of their first message.

Multi-tenant scoping

synap.wrap accepts the standard scoping triple — userId (required), optional customerId, optional conversationId. customerId is required on B2B Synap instances and ignored on single-tenant ones. See Memory Scopes.
const model = synap.wrap(openai("gpt-4o"), {
  userId: "alice",
  customerId: "acme",
});

Failure semantics

The middleware follows the Synap-wide contract:
  • Context fetch degrades gracefully — empty context is injected and the error logged if Synap is unreachable.
  • Turn ingestion surfaces failures — write failures raise SynapIntegrationError so callers know if persistence failed.
This is by design: read failures shouldn’t break a user-facing turn, but silent write failures would let memory drift away from reality.

Next steps

Mastra

SynapMemory and tools for the Mastra ADK.

Claude Agent SDK

Hooks and MCP server for the Claude Agent SDK.

Context Fetch

The retrieval API behind synap.wrap — modes, scopes, and response shapes.

Memory Scopes

How userId, customerId, and conversationId interact across reads.