Skip to main content
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 makes them memory-aware.
@maximem/synap-vercel-adk is a native TypeScript client and works with AI SDK 4 (the LanguageModelV1 interface). With a model from AI SDK 5 or later it still injects context, but it does not record replies, tool calls or reasoning, and it logs one warning. Install ai@^4 and the matching @ai-sdk/* 1.x providers.
Runtime: Node.js 18 or later. Context fetching and turn writes use fetch, so they also run on Edge Runtime and Cloudflare Workers. The gRPC stream opens only on Node.js; elsewhere everything goes over REST. See Installation → JavaScript and 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 each user message and each reply back to Synap as conversation turns, with tool calls, tool results and reasoning on the stream
  • Show the model Synap’s compaction in place of the earlier turns once a conversation has been compacted
  • 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. The package also exports the building blocks the middleware uses (createSynapMiddleware, recordTurn, fetchCompactedHistory, spliceHistory) for code that wires the middleware by hand.

Setup

Install the package:
Import the integration from its package name directly: import { createSynap } from "@maximem/synap-vercel-adk". createSynap initializes the underlying Synap SDK for you, so you don’t import the core @maximem/synap-js-sdk package separately.
Configure your API key. Generate one from the Synap Dashboard.
.env
Initialize the provider once at application startup:
createSynap loads your credentials internally, so you don’t need to manage the SDK lifecycle separately. apiKey is optional: without it, createSynap reads SYNAP_API_KEY from the environment.

Basic integration

The smallest useful integration wraps a model with synap.wrap and uses it like any other Vercel AI SDK model:
On every call, the middleware fetches Synap context, injects it as a system message, proxies the request to the wrapped model, and records the user message and the reply back to Synap as turns. A failed context fetch degrades gracefully (no context is added, the error is logged). A failed turn write is logged and never thrown, so Synap being unreachable cannot break a reply.

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.
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. wrap throws if you pass none of userId, customerId and conversationId. Other options on wrap:

The conversation id

Pass the same conversationId on every call of one conversation. It does two things:
  • Turns are recorded under it, so Synap can compact the conversation.
  • Once Synap has compacted the conversation, the model sees Synap’s compaction block in place of the earlier turns in messages, followed by the turns after the compaction. The last user message and everything after it (a tool call and its result stay paired) go through untouched. Before the first compaction, or if the lookup fails, messages is sent as you passed it.
Without a conversationId there is no conversation to compact or replace. Over REST (Edge, or while the stream is not open) a turn without one is not written. Pass one in production.

The middleware loop

On every call to the wrapped model:
  1. Before: on the first call, starts opening the gRPC stream. Fetches the user’s Synap context (and, with a conversationId, the compacted history), injects the context as a system message, and replaces earlier turns with the compaction when there is one.
  2. Generates: proxies the request to the wrapped model.
  3. After: records the new user message and the reply as turns, plus any tool calls, tool results and reasoning.
Turns go on the gRPC stream when it is open and over REST when it is not, never both. The user message is recorded once even when the turn takes several model calls (tool steps). Tool calls, tool results and reasoning exist only on the stream. Steps 1 and 3 are independent: a failure in either does not block the other.

Provider-agnostic

Wrap any Vercel AI SDK-compatible model:
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 records the reply when the stream ends:

Complete example: chat route with per-request scoping

The pattern below is a typical Next.js chat route on the Node.js runtime. Each request gets its own scope baked into a freshly-wrapped model, so multiple concurrent users cannot leak into each other’s memory:
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 decides the transport. On Node.js the first call opens the gRPC stream. On Edge Runtime the same code runs, and every turn goes over REST (so pass a conversationId).

Advanced patterns

Per-request scoping

For multi-tenant services, build the wrapped model per request and never cache it across users:

The gRPC stream

The first model call on a Node.js process opens a gRPC stream by itself; you do not call listen(). Once it is open:
  • Each turn, tool call, tool result and reasoning step goes on the stream.
  • Anticipated context comes back into the cache, and the next fetch can be answered from it.
The open waits up to 10 seconds. If it fails, calls use REST and nothing retries for 60 seconds. It never throws out of a model call, and it logs one warning. A stream your own code opened with listen() is used as is. To keep the integration on REST, set SYNAP_AUTO_LISTEN=false or pass autoListen: false:
An open stream does not keep a script alive: a Node.js process exits once its work is done, without calling stopListening(). This applies only to the Synap connection (the grpcHost and grpcPort you set, or the default); other HTTP/2 connections in your process behave as before. listen() is optional. Call it once at startup (for example in instrumentation.ts) to have the stream ready before the first request, not per user or per session:
listen() takes no arguments and returns Promise<void>. Scoping stays where it always was, on synap.wrap(). After stopListening() the stream stays closed until you call listen() again; the next model call does not reopen it. On Edge Runtime and in browsers listen() returns without doing anything, and the first model call does not try to open a stream. Everything goes over REST.
Unlike the Python SDK, which raises, listen() here catches a failed open, logs a warning to the console, and falls back to REST. Your app keeps working, just without anticipation. Check synap.isListening rather than assuming the stream is up.
A reported turn becomes long-term memory when its conversation compacts, not when it is written. See What the stream does to memory.

Multi-tenant scoping

synap.wrap accepts the standard scoping triple: userId, customerId and conversationId, at least one of which you must pass. customerId is required on B2B Synap instances and not accepted on B2C (single-tenant) ones, where a call carrying it is rejected with HTTP 400. See Memory Scopes.

Failure semantics

The middleware follows the Synap-wide contract:
  • Context fetch degrades gracefully: no context is added and the error is logged if Synap is unreachable.
  • Compacted history degrades gracefully: if the lookup fails, messages is sent as you passed it.
  • Turn writes never throw: a failed write is logged with a [synap] prefix and your reply is unaffected. If you need to know a write failed, watch the logs.

Going further


Next steps

Mastra

SynapMemory and tools for Mastra.

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.