generateText, streamText, and generateObject all keep working; the middleware makes them memory-aware.
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/generateObjectcall - 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 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..env
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 withsynap.wrap and uses it like any other Vercel AI SDK model:
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.
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 sameconversationId 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,messagesis sent as you passed it.
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:- 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. - Generates: proxies the request to the wrapped model.
- After: records the new user message and the reply as turns, plus any tool calls, tool results and reasoning.
Provider-agnostic
Wrap any Vercel AI SDK-compatible model: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:- The provider is module-scoped, the model is request-scoped.
createSynapruns once;synap.wrapruns per request with the rightuserId. - Streaming is unchanged. No special integration needed;
streamTextand the middleware co-exist transparently. - 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 calllisten(). 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.
listen() is used as is.
To keep the integration on REST, set SYNAP_AUTO_LISTEN=false or pass autoListen: false:
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.
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,
messagesis 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
- Patterns overview: reusable memory patterns across frameworks.
- Cookbook overview: end-to-end worked examples.
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.