Migrating from Mem0 to supermemory: a production playbook for the v5 API

To move from Mem0 Platform to supermemory, map each Mem0 entity scope to one supermemory namespace, import your Mem0 memories as documents with stable IDs, point your read path at POST /ns/{namespace}/search, and run both systems side by side until the new one passes your tests. This playbook uses supermemory's v5 API (namespace, id, filter), the only version to build on now: v3 and v4 shut down on December 31, 2026, and the v5 migration guide covers the differences.
If you followed an earlier version of this guide, your code uses v3/v4 names such as containerTag, customId and q, and the direct memory endpoint that v5 removed. Run npx supermemory@latest migrate in that repo: it scans for v3/v4 calls and hands your coding agent a migration prompt built for the project.
This guide covers Mem0 Platform. If you self-host Mem0 Open Source, document the vector store, graph store, model providers and persistence layers in your deployment first, because a platform export can't represent that whole system. If you're still deciding whether to switch, read why Scira moved to supermemory and our supermemory and Mem0 comparison first. For the longer write-up, read how supermemory and Mem0 compare on features. The short version of the import scripts is in the docs; this article is about moving production data and traffic without making the move irreversible.
What changes in a Mem0 migration?
The central change is identity. Mem0 can scope a memory by user_id, agent_id, app_id and run_id at once. supermemory scopes everything by one namespace, which goes in the URL of every v5 request, and uses metadata for anything else you need to filter on or trace back.
| Mem0 Platform | supermemory v5 | What you have to decide |
|---|---|---|
user_id, agent_id, app_id, run_id |
One namespace in the path (/ns/{namespace}/...), plus metadata |
A deterministic namespace rule, before importing anything |
add(messages, user_id=...) |
POST /ns/{namespace}/document with content and a stable id |
One document per conversation. Repeating the id adds to that document |
| Extracted memory record | A small document per fact, since v5 has no direct memory write | Stable IDs, so a retry isn't a second copy |
search(query, filters, top_k) and its score |
POST /ns/{namespace}/search with query, limit, searchMode, threshold and filter. Results carry similarity |
A rewritten adapter and retuned thresholds |
get_all(filters) |
POST /ns/{namespace}/list/memories |
One namespace per call, paged up to 100 at a time |
update(memory_id, text) |
PATCH /ns/{namespace}/document/{id} |
The new content replaces the source and is reprocessed |
delete(memory_id) |
Forget memories (DELETE /ns/{namespace}/memories) or delete the source (DELETE /ns/{namespace}/document) |
Whether your app means hide or erase |
expiration_date |
No field sets an expiry when you write | A scheduled job, described below |
Don't start the backfill until that mapping is written down. A migration script can move every record successfully and still break tenant isolation if the namespace rule is wrong.
Is this guide right for your setup?
Go ahead when every memory read and write can be mapped to a stable user, tenant, project or agent boundary.
Pause for architecture work when:
- queries depend on several Mem0 entity dimensions at once
- the application expects to search across arbitrary tenants
- nested metadata drives authorization or retrieval
- exact graph topology must be preserved
- deletes must be permanent the moment the application requests them
A v5 request reads or writes one namespace. That's useful isolation, but the namespace has to match the boundary your authorization model already enforces, and code that searched across several scopes needs one call per namespace with the results merged. Graph edges and provider-specific scores aren't portable data formats.
What should you inventory first?
Write an inventory before touching production:
- Record the Mem0 SDK and API versions in use.
- List every user, agent, app and run scope that owns memories.
- Choose one scope order for the export, and deduplicate globally by Mem0 memory ID before counting records.
- Separate raw conversations and documents from already-extracted memory facts.
- Find metadata with nested objects that will need flattening or serializing.
- Capture a golden set of real queries, expected facts, current ranks, empty-result rate, error rate, and p50 and p95 latency.
That last step is the migration's acceptance test. Without a baseline, "the import completed" quietly replaces "the product still remembers correctly."
Mem0's v3 list endpoint takes entity IDs inside a filters object, needs at least one entity filter, and returns pages of up to 200 records. Overlapping user, agent, app and run queries can return the same memory more than once, so write each source ID to the snapshot once. Check the response fields from your own API version before mapping identifiers: the list endpoint returns the run as session_id, while search returns the same value as run_id.
How should Mem0 IDs map to namespaces?
Pick one deterministic rule and use it everywhere. Common patterns:
user:{user_id}for a single-tenant consumer apporg:{org_id}:user:{user_id}for user memory inside an enterprise tenantagent:{agent_id}for an agent-owned knowledge baseapp:{app_id}:user:{user_id}when the same person must stay isolated across products
Store the dimensions that don't define access as flat metadata, such as mem0_agent_id, mem0_app_id and mem0_run_id. A v5 namespace is up to 100 characters of letters, numbers, underscores, colons and hyphens (API reference). If a source ID falls outside that, encode or hash it without creating collisions, and keep the mapping. Never derive the namespace one way in the importer and another way in the live request path.
A namespace scopes what's stored and searched. It doesn't authenticate the caller. Derive it from the authenticated identity on the server, check access before each operation, and test cross-tenant requests before cutover. The namespaces docs cover the model in more detail.
How do you export from Mem0 without losing anything?
For the backfill, page through Mem0's memory list endpoint in the scope order you chose, deduplicate globally by memory ID, and write the raw records to an immutable JSONL file. Expired memories are hidden by default, so pass show_expired if the migration needs them. Memories that Mem0's Dream feature merged into another one are hidden from reads by default too (its docs describe an include_merged flag that returns them), while superseded memories come back by default with replaced_by set. Reconcile your counts before calling the snapshot complete. Keep for each record:
- the memory text and source ID
- user, agent, app and run identifiers
- categories and metadata
- creation, update and expiration timestamps
- the lifecycle state your application depends on
Mem0 also has structured memory exports. Those are useful when you want to reshape memories into a schema, such as a profile, but they aren't a one-to-one dump, because the schema and export instructions change the result. If you use one, poll until the job completes and download it promptly.
Keep the raw snapshot even after supermemory is live. It's the only reliable starting point for a rollback or a corrected re-import.
How do you import Mem0 memories into supermemory?
v5 has no endpoint that writes a memory directly. Memories come from documents, so each Mem0 fact becomes a small document of its own (v5 guide). This script reads the snapshot, groups it by namespace (each v5 request writes to one) and imports it in batches of up to 600:
import { readFileSync } from "node:fs"
import { Supermemory } from "supermemory" // npm i supermemory (v5 SDK, 5.0.0 or later)
type Mem0Record = {
id: string
memory: string
user_id?: string | null
agent_id?: string | null
app_id?: string | null
session_id?: string | null // the list endpoint's name for run_id
categories?: string[] | null
created_at: string
expiration_date?: string | null
replaced_by?: string | null
}
const supermemory = new Supermemory() // reads SUPERMEMORY_API_KEY
// One rule for the namespace, shared with the live request path
const namespaceFor = (userId: string) => `user:${userId}`
const snapshot: Mem0Record[] = readFileSync("mem0-snapshot.jsonl", "utf8")
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line))
// A v5 request writes to one namespace, so group the snapshot first
const byNamespace = new Map<string, Mem0Record[]>()
for (const r of snapshot) {
if (!r.user_id || !r.memory || r.replaced_by) continue // superseded facts stay in the snapshot; log records without a user_id, they need their own namespace rule
const ns = namespaceFor(r.user_id)
const list = byNamespace.get(ns) ?? []
list.push(r)
byNamespace.set(ns, list)
}
for (const [namespace, records] of byNamespace) {
for (let i = 0; i < records.length; i += 600) {
const res = await supermemory.documents.batchAdd(namespace, {
documents: records.slice(i, i + 600).map((r) => ({
id: `mem0_${r.id}`, // stable, so a retry updates this document instead of adding a copy
content: r.memory,
date: r.created_at, // when the fact is from
metadata: {
source: "mem0",
mem0_id: r.id,
mem0_agent_id: r.agent_id ?? "",
mem0_app_id: r.app_id ?? "",
mem0_run_id: r.session_id ?? "",
mem0_categories: r.categories ?? [],
mem0_expires_on: r.expiration_date ?? "",
},
})),
})
for (const item of res.results) {
if (item.error) console.error(namespace, item.id, item.error, item.details) // send to a rejects file
}
}
}
A few things the script relies on:
- The
idmakes retries safe. Sending the sameidagain adds to or diffs that document instead of creating a copy, and unchanged content isn't billed twice (billing docs). Because the Mem0 ID is in the document ID, your migration ledger is mostly the ID rule itself. datekeeps when the fact is from, so recent context stays recent after the import.- Metadata values must be flat: strings, numbers, booleans or arrays of strings. Flatten or serialize nested Mem0 metadata, and write records you can't convert to a rejects file instead of truncating them.
- Read every result. A batch can partly succeed, and results list accepted documents first, then failed ones, so match them by
id, not by position. - Memories form after the request returns. With the default
dreaming: "dynamic", memories form in the background, so wait until each document'ssystem.dreamingStatusisdonebefore you test recall on it.dreaming: "instant"processes each document straight away for one extra operation per document, which helps when you're running parity tests on a small cohort (v5 guide, pricing). - Each fact is extracted again. supermemory reads the imported text and forms its own memories from it, so compare a sample of them with the Mem0 originals before you import everyone.
How should the live write path change?
Use two write paths on purpose.
For new conversations and source documents, add the content to the user's namespace with a stable id, usually the conversation ID. Repeating that id appends to the same document, so one conversation stays one document. Decide whether each write sends the whole conversation or only the new turns, and keep that the same for every write to an id.
For exact facts your application already knows, add each one as its own small document with a stable id, the same way the import does. Use dreaming: "instant" when the next request needs it.
Processing is asynchronous. A successful write means the work was accepted. A document's system.status reaches done when it's processed, and system.dreamingStatus tracks memory formation separately: under the default dynamic mode, memories can still be forming in the background after status reaches done, so wait on dreamingStatus when the next step reads memories or the profile.
During the migration, dual-write new events to both providers, and keep one of them authoritative for reads until the new path passes the full evaluation.
How should the read path change?
The adapter needs more than renamed fields. Mem0's query stays query, entity filters become the namespace plus a typed filter on metadata, top_k becomes limit, and Mem0's score has no direct equivalent: v5 results carry a similarity from 0 to 1.
// Mem0: search(query, filters={"user_id": ...}, top_k=5)
export async function recall(userId: string, query: string) {
const { results } = await supermemory.search(namespaceFor(userId), {
query,
searchMode: "memories", // v5 defaults to hybrid; pin the mode while you compare
limit: 5,
threshold: 0.3, // the v5 default, written out so you can tune it
})
return results.map((r) => ({ text: r.memory, similarity: r.similarity })).filter((r) => r.text)
}
v5 search defaults to hybrid, which returns memories and source chunks in one ranked list. Pin searchMode: "memories" while you compare against Mem0, then test hybrid on its own. Don't copy a Mem0 score cutoff into threshold: the two are calibrated differently, so tune it against the golden set you captured.
For validation, score the retrieved context before the answer model sees it. A fluent model can hide a bad retrieval by guessing convincingly.
How do you validate before cutover?
Run both systems in parallel on the same production-shaped queries, and measure:
- exact-fact recall and top-k relevance
- stale or contradictory memories
- empty results and unexpected extra results
- tenant isolation and metadata filters
- expiry behavior
- ingestion success and time until a fact is searchable
- latency and error rates as your application sees them
- duplicate documents after retries
Add a small lifecycle fixture to the golden set: a saved preference, a correction, a temporary exception and a deletion request, with the same questions asked after each step. When an answer is wrong, find out where: a connector, the namespace, processing that hadn't finished, retrieval, or the final prompt. Fixing the boundary that failed is part of the migration, whichever provider you end up on.
Backfill a small group of namespaces first. Shadow reads without showing the new response to users. When the acceptance criteria pass, move a low-risk cohort behind a feature flag and keep rollback to Mem0 available. Switching memory infrastructure covers the organizational side. The technical rule is simpler: don't move all reads and writes on the same day.
Which lifecycle differences need explicit handling?
| Operation | Mem0 | supermemory v5 | Decision required |
|---|---|---|---|
| Update | Updates a memory record, and Dream can mark older facts superseded | PATCH the source document: the new content replaces it and is reprocessed. Memories keep version history (isLatest, earlier and later versions) |
Whether your application shows history |
| Forget | delete removes the memory |
memories.forget removes memories from normal recall and keeps their audit history. Search can still return them with include.forgotten |
Don't treat forgetting as an erasure request |
| Erase | Deletes the record | documents.delete permanently removes the document and everything derived from it |
Use this for erasure requests |
| Expire | expiration_date hides a memory from the day after that date, in UTC. Nothing is deleted |
No field sets an expiry when you write | Keep the date in metadata, run a daily job that forgets the memories of expired documents, and test it |
Mem0 memory IDs, your mem0_ document IDs and supermemory memory IDs are three different things. Keep the type next to every ID in your ledger, or deletion code will eventually target the wrong one. Forgetting the memories that came from one imported fact looks like this:
// Hide a fact from recall but keep its history (Mem0's expiry behavior)
export async function forgetFact(userId: string, mem0Id: string) {
const doc = await supermemory.documents.get(namespaceFor(userId), `mem0_${mem0Id}`, { include: ["memories"] })
const ids = (doc.memories ?? []).map((m) => m.id)
if (ids.length) await supermemory.memories.forget(namespaceFor(userId), { ids })
}
To erase it instead, call supermemory.documents.delete(namespace, { ids: ["mem0_..."] }). Deletion works once a document has finished processing, and the response lists any IDs that failed, so check it.
What should the rollback plan contain?
A rollback plan needs four concrete assets:
- the immutable source snapshot
- the source-to-destination ID rule and ledger
- a reversible read and write feature flag
- an append-only log of writes created after the snapshot
During dual-write, declare which provider is the source of truth. If you roll back, switch reads first, restore the original write path, then replay only the events recorded after the snapshot. Don't delete Mem0 data until the rollback window closes and the team has verified counts, retrieval quality, lifecycle behavior and tenant isolation.
What's the safest next step?
Migrate one representative tenant: not the easiest one, and not the largest. Import its facts, replay its real queries, test updates and forgetting, and measure the engineering work it takes to keep the system healthy. Then decide from the evidence, and keep the first cutover small enough to reverse.
Common questions
Can I migrate from Mem0 to supermemory without downtime?
A dual-write and shadow-read rollout can avoid a planned outage, but no migration is zero-downtime by default. Keep Mem0 serving reads while you backfill and validate supermemory, then move a small cohort behind a reversible feature flag.
How do Mem0 entity IDs map to supermemory namespaces?
Choose one namespace as the isolation boundary, such as org:{org_id}:user:{user_id}. Keep the other Mem0 dimensions, such as agent_id, app_id and run_id, as flat metadata, or put them in the namespace when they affect who can read the memory.
Will supermemory keep Mem0 memory IDs and timestamps?
Put the Mem0 ID in the document ID (for example mem0_<id>) and in metadata, and pass the original created_at as the document's date. supermemory gives the memories it forms their own IDs.
Is there a tool that migrates for me?
npx supermemory@latest migrate moves code from supermemory's v3/v4 API to v5. It doesn't read Mem0, so for the data itself use the import script above. The docs scripts are a shorter start, but they read a structured export and put every memory in one namespace, so adapt them to your namespace rule first.
Does this guide apply to Mem0 Open Source?
Not directly. It covers Mem0 Platform's APIs. Mem0 Open Source can use different vector stores, models, embedders and persistence layers, so its export plan has to match the deployed stack.