---
title: One memory per end user: keep your users apart, and delete one on request
slug: one-memory-per-end-user
date: 2026-10-12
author: Semurg
section: how-to
tags: [memory, multi-user, privacy, delete, how-to]
summary: An app with many users needs each user's memory kept apart and erasable on request. Name the end user with user_id on any memory call, pin an MCP connection to one user, and delete one user's whole memory in one call.
status: published
covers: [route:POST /v1/memory/remember, route:POST /v1/memory/recall, route:POST /v1/memory/delete_user]
---
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.

> Diagram one-key-many-users: 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.

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 runnable kind=shell-node needs=token
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"}]}}'
```

```expect
expect: the facts are stored in Alice's memory
assert: json:.ok==true
```

Asked as Alice, the memory answers:

```bash runnable kind=shell-node needs=token
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"}'
```

```expect
expect: Alice's memory knows Dana Whitfield
assert: json:.ok==true and ([..|strings|ascii_downcase|select(contains("whitfield"))]|length>0)
```

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

```bash runnable kind=shell-node needs=token
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"}'
```

```expect
expect: Bob's memory holds nothing about Dana Whitfield
assert: json:([..|strings|ascii_downcase|select(contains("whitfield"))]|length==0)
```

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.

> Diagram pinned-connection: 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.

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 runnable kind=shell-node needs=token
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"}'
```

```expect
expect: Alice's memory is deleted, with counts of what was removed
assert: json:.ok==true and .state=="deleted" and .user_id=="alice"
```

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

- [Give your agent memory over MCP](/blog/agent-memory-mcp-five-minutes).
- [Connecting over REST](/blog/connect-over-rest): the token and every route.
