Blog / How-to guides

One memory per end user: keep your users apart, and delete one on request

12 October 2026 · Semurg · 4 min read · Markdown

Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)

memorymulti-userprivacydeletehow-to

If you build an app on Semurg's memory, your app holds one API key and has many users. Each of them should have a memory of their own: what one user told your assistant must never come back in another user's answer, and when a user asks you to delete what you hold about them, you need to delete all of it, at once, without touching anyone else's.

Your app holds one API key and names the end user on every memory call, so Alice's scope and Bob's scope of the same name are separate partitions of one store, and deleting Alice removes every scope she has without touching Bob's.One store, partitioned, never copiedYour app, one API keyCalls with user_idAliceCalls with user_id BobDelete end user AliceAlice's support scopeBob's support scopeevery scope she has

Semurg does this with one optional field, user_id, on every memory call, and one call that deletes an end user.

Name the end user on every call

Add user_id to any memory call: remember, recall, relate, forget, context or document. Every scope you name is then that end user's own. Two end users never meet, even when your app uses the same scope name for both, and nothing you write without a user_id is visible inside an end user's memory, or the other way round. It is a partition of the same store, not a copy.

Alice tells your assistant how she wants to be contacted:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"assistant","user_id":"alice",
       "facts":{"entities":[{"name":"Dana Whitfield","type":"Person"},{"name":"Email","type":"Channel"}],
                "relations":[{"source":"Dana Whitfield","target":"Email","type":"PREFERS"}]}}'

Expected: the facts are stored in Alice's memory

Asked as Alice, the memory answers:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"assistant","user_id":"alice","subject":"Dana"}'

Expected: Alice's memory knows Dana Whitfield

Asked the same question as Bob, in the same scope, it knows nothing about her:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"assistant","user_id":"bob","subject":"Dana"}'

Expected: Bob's memory holds nothing about Dana Whitfield

A user_id is any text without a colon, up to 256 bytes. An empty one, one with a colon, one too long or one that is not text is refused with 422 before anything is read or written, and the answer says which: user_id is empty, user_id carries ':', user_id is longer than 256 bytes, user_id is not a string or user_id is not UTF-8. Use your own stable id for the user, not their email address, and set it in your server code from the signed-in user, never from what a user or an agent sends: a call reaches the user_id it names, and anyone holding your token can name any of your users.

Pin an MCP connection to one end user

When your app hands an agent a memory connection over MCP on behalf of one user, set the X-Semurg-User header on that connection. The header is then the end user for every tool call on it: a call with no user_id takes the header's, and a call that names a different user_id is refused with 422. So an agent working for Alice cannot reach Bob's memory through that connection, however its tool calls are written: give the agent only the pinned connection, never your token itself.

An MCP connection with the X-Semurg-User header is pinned to one end user: a call without a user id takes the header's, and a call that names a different user is refused.An agent working forAliceConnection pinned:X-Semurg-User AliceA call with no user idA call naming BobAlice's memoryRefused

Deleting an end user is not an MCP tool. It is a decision your app or a person makes, so an agent's tool call can never trigger it.

Delete one end user

When a user asks you to delete what you hold about them, one call removes their whole memory, across every scope they have:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/delete_user \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

Expected: Alice's memory is deleted, with counts of what was removed

The reply counts the scopes, entities, documents and records it removed. Your key can only delete its own end users, never another key's. Asking again for the same user answers already_deleted; a user your key never wrote answers 404 with no such end user, and nothing is written. A call without a user_id answers 422 with missing 'user_id'. If the deletion stops partway, the answer is 503 with delete-user stopped before it finished and says the user is not deleted, so you run the same call again; it never reports a partial deletion as done.

Deleting appends small records that mark the data as gone, so no call serves it again from the moment the call answers. The call does not overwrite anything: Semurg never rewrites data in place. On this release the deleted bytes stay on the node's disk, unreachable through every call, until a release ships the step that reclaims that space.

From a client library

The Elixir client takes user_id on its memory calls and has delete_user/2; the C# client has DeleteUser. Other client libraries send the same JSON bodies to the same routes.

What it does not do yet

  • user_id keeps your app's users apart inside one key. It is not a security boundary between apps or teams: use separate API tokens for that.
  • The public sandbox, a node with no token reached from another machine, does not delete memory.
  • Remove a deleted user's bytes from the disk itself: on this release they stay there, unreachable, until the reclaim step ships. If your obligations require the bytes physically gone, this release does not meet them yet.

Next

How these commands were checked

Our test farm ran each of the 4 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.

Related