MemorySync

Scoping Every Memory: Organization, Project, End User

Every MemorySync memory is scoped to an organization, a project, an end user and the key's environment. How each is set, why the API enforces it, and where it stops.

Published on 6 min read

Scoping Every Memory: Organization, Project, End User
On this page

A memory API that serves many people has one job before all others: a question asked for one person must never come back with another person's memories. MemorySync scopes every memory to an organization, a project and an end user, inside the environment of the API key that wrote it.

The examples use a coaching app with two end users, alex and sam, and a staging copy of the app next to production. Short names keep the code readable. In production, use opaque ids as described in Choosing End-User IDs You Will Not Regret.

Four values on every request

ScopeWhere it comes fromWhat it separates
OrganizationThe API key. Every key belongs to one organization.Companies, with their members, keys and connected sources
ProjectThe key, if it was created inside a project. Otherwise the X-Project-ID header.Applications or workloads
End userThe X-End-User-ID header, or an end_user_id body fieldThe people your application serves
EnvironmentThe key: production or developmentLive data and test data

A key created while a project is selected in the dashboard is locked to that project, so calls with it need no project header. An organization-wide key must name the project on every call with X-Project-ID, whose value is proj_ followed by 16 hexadecimal characters. Sending a project id that differs from the one locked into the key is rejected.

The environment never comes from the request body. It belongs to the key, so a development key and a production key see different data, even inside the same project.

With a project-locked key, the end user is the only scope your code sets per request. The organization, the project and the environment all come from the key the server loads, so choosing the key is choosing the environment. Load it from each deployment's configuration, never from a value a request can influence.

Python
import os
from memorysync import MemorySyncClient
# A key created inside a project: the project comes from the key.
ms = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_API_KEY"],
base_url="https://api.memorysync.io",
end_user_id="sam",
)
# An organization-wide key names the project on every call (sent as X-Project-ID).
ms = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_ORG_API_KEY"],
base_url="https://api.memorysync.io",
project_id="proj_0a1b2c3d4e5f6a7b",
end_user_id="sam",
)

The end user is required

Every API-key call must name an end user. A query sent without one gets a 400 with this body, cut here after the second sentence of the message:

JSON
{
"detail": {
"error": {
"code": "MISSING_END_USER_ID",
"message": "API-key and OAuth client-credentials callers MUST identify the end user via the 'X-End-User-ID' header (or 'end_user_id' body field). Memories are always scoped to (tenant, project, user) ...",
"header": "X-End-User-ID"
}
}
}

The call does not fall back to a default user such as the owner of the key. The Python SDK raises ValidationError and puts the parsed body on the exception's response attribute. An end-user id longer than 128 characters is refused the same way, with INVALID_END_USER_ID.

Treat either error as a bug in your server code rather than something to retry. Sending the same request again gets the same 400.

Why the API enforces it

Many multi-tenant leaks come from a missing filter: a new endpoint, a background job or a debugging script that forgot to restrict a query by user. A filter your code adds is a filter your code can forget.

So MemorySync requires the end user on every API-key request. Reads, searches and deletes are filtered to the end user, project and environment on the server, and a call that names nobody fails with the 400 above instead of searching everyone. Deletes follow the same rule: forget() never reaches another end user's memories.

Failing loudly is the point. A 400 in staging is a bug you fix the same day. A search across every user in production is an incident.

An isolation check you can run

Scoping is easy to test, and the test belongs in CI against a development key. Write a memory for one end user, search as another, then search as the first:

Python
import os
from memorysync import Memory, MemorySyncClient
def client_for(end_user_id: str) -> MemorySyncClient:
return MemorySyncClient(
api_key=os.environ["MEMORYSYNC_DEV_API_KEY"], # a development key
base_url="https://api.memorysync.io",
end_user_id=end_user_id,
)
with client_for("alex") as alex, client_for("sam") as sam:
written = alex.add("My favourite programming language is Rust.")
assert isinstance(written, Memory)
seen_by_sam = {m.id for m in sam.query("favourite programming language", k=5).memories}
seen_by_alex = {m.id for m in alex.query("favourite programming language", k=5).memories}
assert written.id not in seen_by_sam
assert written.id in seen_by_alex

In this example, the memory about alex's favourite language is not among sam's results, and alex's own query returns it. Sam's search is not empty, though. It returns sam's own closest memories, which may have nothing to do with programming, so assert on ids rather than on an empty list.

Put checks for the other scopes next to it: a call with no end user fails with a 400, and a memory written with the development key is not found with the production key.

Run the suite again after any change to how your server resolves the end user, such as a new sign-in provider, a new background job or a new route that writes memory. Those are the changes that break isolation without anyone noticing.

What scope does not do

Scope filters by the end user you name. It does not know who is signed in to your application. If a route accepts ?user=sam from the browser and forwards it, every request is correctly scoped and the app still leaks, because anyone can type another name. Your application decides which end user a request is for, so authorize first, then call MemorySync from your server.

  • session_id groups the turns of a conversation. It is not an access boundary.
  • Tags and metadata describe a memory. Use them for traceability, not access control.
  • Memories from connected sources are organization knowledge. Inside a project, a search includes the project's memories plus the organization-wide memories from connectors such as GitHub, Google Drive, Notion and OneDrive, so treat anything you connect as reachable from every project in the organization.
  • Returned memories are data. Treat their text as information about the user, never as instructions to your system.

Common layouts

NeedHow to scope it
Memory for each personOne stable, opaque end-user id per person, resolved from the signed-in session
Conventions a whole team sharesA workspace-level end-user id such as team-engineering, with your server deciding who may read and write it
A staging copy of the appA development key for staging and a production key for production
Two unrelated applicationsTwo projects, each with a key locked to it

Per-user memory is the default and needs no extra setup. Shared team memory adds one more end-user id. Because a search covers one end user at a time, the assistant queries the person and the team separately and merges the two result lists.

For staging, never copy the production key into staging configuration. With a development key, a test that writes nonsense for sam cannot touch the production sam, and a staging bug cannot read production memories.

For two applications that must never share memory, give each its own project and a key locked to it. A bug in one application cannot then reach the other's memories, because its key cannot name the other project.

Next step

Read Scopes & End Users in the docs, then add the isolation check above to your test suite with a development key.

Share article

Blog

Try it on your own workload

The free tier stores real memories and returns real rankings. One write and one retrieval to start.

Start free. No credit card required.