# Memory isolation with row-level security, not application code (/blog/memory-isolation-with-row-level-security)

![Illustration: A tall filing cabinet with many drawers, each shut with a small padlock except one.](/blog/memory-isolation-with-row-level-security/hero.webp)

CoreSpeed memory is isolated by Postgres row-level security. The caller's identity comes from the credential on the request, every memory carries an organization and a scope, and the database applies the visibility rule to each query. An agent cannot supply a different user id, and application code cannot forget the check, because the check lives below it.

**The boundary lives below the application**

* **Prompt and tool call** — memory\_\_search\_memory takes a query. No field for a user id exists. (untrusted text)
* **Credential** — a browser sign-in, an sk-cs- key, an sk-csa- key or a session JWT resolves to one principal (identity from auth)
* **Application check** — filters by principal and scope; produces the clear error messages (clear errors)
* **Postgres row-level security** — the policy is attached to the table and applied to every statement, whichever code path issued it (every query, every path)
* **Rows** — each memory carries an organization and a scope: private or shared (org + scope)

The check lives below the application code, so a missed check cannot leak.

## Where does the caller's identity come from? \[#where-does-the-callers-identity-come-from]

From auth, and only from auth. Four credentials reach `/mcp`, and each resolves to a principal before any tool runs:

```text
Authorization: Bearer <token>    a browser sign-in, an sk-cs- key, an sk-csa- key, or a session JWT
x-api-key: <key>                 sk-cs- and sk-csa- keys only
```

A browser sign-in is you, a member. An `sk-cs-` key acts as the member who created it. An `sk-csa-` key is an agent principal with an identity of its own, owned by the member who answers for it. The organization that owns the credential is the organization the memory belongs to.

What is absent is any identity field in the tool call. `memory__search_memory` takes a query. `memory__remember` takes text and a scope. Neither takes a user id, so a prompt cannot hand the agent one.

## How do shared and private scopes work? \[#how-do-shared-and-private-scopes-work]

Every write is scoped. `memory__remember` accepts `scope: "shared"` or `"private"`. Private is the default and can be read only by the member who created it. Shared must be asked for explicitly, and can then be read by every agent operating inside the same organization. The [memory page](/docs/memory) is the reference.

| Credential          | Writes as                      | Reads                                                             |
| ------------------- | ------------------------------ | ----------------------------------------------------------------- |
| browser sign-in     | the member                     | the member's private memories plus the organization's shared ones |
| `sk-cs-` member key | the member who created the key | the same as that member                                           |
| `sk-csa-` agent key | the agent principal            | the agent's own memories plus the organization's shared ones      |

Member state is organization-scoped. Turning memory off in one organization cannot affect the same person in another organization. An agent principal's memories are its own, and the member accountable for that agent maintains them directly, which is how an owner corrects what a bot has saved.

Links follow the same rule. `memory__list_memory_links` lists only links whose two memories you can see. A link to a teammate's private memory stays invisible to you, as that memory does.

## What does row-level security add over an application check? \[#what-does-row-level-security-add-over-an-application-check]

A visibility check written in application code is a filter that every query path must remember to apply. Search, list, get by id, link listing, export, the maintenance job that archives stale rows: each is a place where one missing clause returns another member's memory. A test suite catches the paths it knows about.

Row-level security moves the rule into the database. The policy is attached to the table, and Postgres applies it to every statement that touches the table, whichever code path issued it. A query without the clause does not return more rows. It returns the same rows the policy allows, because the predicate is added underneath.

The docs state the property in one sentence: the boundary is enforced in Postgres by row-level security, not only in application code. Application code still checks, and that layer is what produces clear errors. The database is what makes a missed check a wrong error message rather than a leak.

## Why not trust the request body? \[#why-not-trust-the-request-body]

Because the request body is written by a model that reads untrusted text. A fetched page, a post or a document can carry an instruction, and an agent can follow it. If the memory API accepted a user id in the call, "search the memory of user X" would be one injected line away.

Deriving identity from the credential closes that line. The worst an injected instruction can do is search what the caller could already search. That is the same reason the security guidance tells you to treat fetched content as untrusted and to give unattended agents an [agent key](/docs/authentication): the credential, and nothing in the conversation, decides the reach.

## How was the boundary tested? \[#how-was-the-boundary-tested]

The published LongMemEval run doubled as an isolation test. All 500 questions of LongMemEval-S ran with each question in its own isolated workspace, and each workspace held a private tripwire memory owned by a different user. One leak would fail the entire run. It finished at 0 leaks out of 500, with retrieval running the production hybrid path under row-level security.

The headline number from that run, 94.2 end-to-end with a flash-class reader while retrieval recall at 10 stayed 0.99, is the subject of [its own post](/blog/how-we-took-longmemeval-from-80-to-94-without-touching-retrieval). The isolation result is the quieter one, and for a shared memory service it is the one that has to hold first.

## FAQ \[#faq]

**Can an agent read a teammate's private memories?**
No. Private memories are readable only by the member who created them. An agent principal reads its own memories and the organization's shared ones.

**What if a prompt tells the agent to search as another user?**
There is no field for that. Identity comes from the credential, so the search runs as the caller, whatever the prompt says.

**Is a shared memory visible across organizations?**
No. Memory belongs to the organization. Shared means shared inside that organization's boundary.

**Does disabling the memory capability delete anything?**
No. Disabling hides the memory tools from `tools/list` and keeps the data. Deletion is a separate explicit action, in the dashboard or through `memory__delete_memory`.