Namespaces and multi-user patterns
One namespace is one memory. How to map your users onto them, and what happens at the edges.
A namespace is one memory: its own git repository, its own index, its own vectors. Two
namespaces are two repositories under two different storage prefixes, so isolation is a
storage boundary rather than a WHERE clause somebody can forget to write.
An account has many namespaces. A credential resolves to an account, never to a namespace — which namespace a request touches is decided by your server, per request.
The rules
- Names are
[a-z0-9-], 1–64 characters. Anything else is400 invalid_namespace. Uppercase is not normalised, it is rejected. Neither is_or.accepted. - They are created explicitly. Reading or writing an uncreated namespace is
404 namespace_not_found, not an implicit create. A typo in a user id must not mint an empty memory and report success. - Creation is idempotent.
201the first time,200after; both are success. defaultis an ordinary name. Omittingnamespacemeansdefault, anddefaultmust be created like any other.- Writes to one namespace serialise; different namespaces run in parallel.
- There is no delete. Nothing in the API removes a namespace or a memory today.
Creating and listing
curl -s -X POST "$GITLOOM_API/v1/namespaces" \
-H "Authorization: Bearer $GITLOOM_KEY" \
-H "content-type: application/json" \
-d '{"namespace":"user-8213"}'
{"namespace":"user-8213","created":true}
A second call:
{"namespace":"user-8213","created":false}
```bash
curl -s "$GITLOOM_API/v1/namespaces" -H "Authorization: Bearer $GITLOOM_KEY"
```{"namespaces":["default","user-8213"]}
The listing returns the first 200 namespaces and truncates silently. There is no pagination and no count. If you expect more than 200, do not use this endpoint as your source of truth — your own user table already knows which namespaces exist.
Pattern 1: one namespace per end user
The default, and the shape the isolation model was built for.
const ns = `user-${user.id}`
await gl("/v1/namespaces", { method: "POST", body: JSON.stringify({ namespace: ns }) })
Why per-user rather than one shared memory with a user tag:
- Isolation is structural. A retrieval cannot return another user's memory, because the other user's memories are not in the repository being searched.
- Semantic search scans linearly in the number of stored vectors. One namespace per user keeps each index small — this is a large part of why the model is shaped this way.
- Write serialisation is per namespace. One shared memory would serialise every write in your product behind one queue group.
- Erasure is tractable. When a delete endpoint exists, "forget this user" is one object prefix, not a filtered rewrite.
Deriving the name deterministically from your own user id is the important part. Do not
store the namespace name as a separate mutable field, and do not use anything the user
controls — an email address is not [a-z0-9-].
// Safe: your own opaque id, prefixed so it can never collide with an internal name.
const ns = `user-${user.id}`
// Unsafe: user-supplied, wrong charset, and a 400 you will discover in production.
const ns = user.email
If your ids are UUIDs, they already fit the charset once lowercased. If they are
integers, prefix them — a bare 8213 is legal but tells you nothing in a log line.
Creating it at the right moment
Create the namespace when you create the user, not on the first write. The write path
checks existence at the front door and returns 404 while you are still listening, so
the failure is loud — but recovering from it mid-conversation means the turn the user
just had is the one you lose.
Creation is idempotent and cheap, so the belt-and-braces version is fine too:
try {
await memory.remember(turns, { namespace: ns })
} catch (e) {
if (e instanceof GitloomError && e.isNamespaceNotFound) {
await memory.createNamespace(ns)
await memory.remember(turns, { namespace: ns })
} else throw e
}
Pattern 2: one namespace per project or workspace
When the memory belongs to a team rather than a person — a shared assistant in a Slack channel, a per-repository code agent — name the namespace after the workspace:
account "acme"
├── workspace-42 ← shared by everyone in that workspace
├── workspace-91
└── default
The tradeoff is that everything one member tells the assistant is retrievable by every other member. That is usually the intent for a shared bot; it is never the intent for a consumer product.
Pattern 3: one namespace, single-user
A personal tool, a single-tenant internal agent. Create default, never send a
namespace parameter, and stop thinking about it.
curl -s -X POST "$GITLOOM_API/v1/namespaces" \
-H "Authorization: Bearer $GITLOOM_KEY" \
-H "content-type: application/json" -d '{}'
Anti-pattern: a namespace per conversation
Do not do this. A namespace per session throws away the thing GitLoom is for — a memory
is valuable precisely because it spans sessions. Use session_id on the write to tag
which conversation a memory came from; the namespace is the person, not the chat.
What isolation does and does not give you
Does: one namespace's retrieval cannot see another's content, at the storage layer. A container caches up to four warm namespaces at once and keys that cache by the exact account-and-namespace pair, so a shared Lambda container cannot cross the line either.
Does not: protect you from your own routing bug. An API key is account-wide. If your server passes the wrong namespace for the request it is handling, GitLoom will answer faithfully with the wrong user's memories. The namespace should be derived from the authenticated session on every request, in one place, and never accepted from a request parameter your client controls.
// Good: derived server-side from the session.
const ns = `user-${session.userId}`
// Catastrophic: the client chooses whose memory to read.
const ns = req.query.namespace
Quotas are per account, not per namespace
The Free plan's 100 writes / 2,500 reads / 1,000 memories / 25 playground chats per month are counted across every namespace in the account. Adding users does not add allowance — see Plans and pricing for every tier, and for how paid plans overflow into a wallet instead of stopping. Check where you stand:
curl -s "$GITLOOM_API/v1/usage" -H "Authorization: Bearer $GITLOOM_KEY"
{
"period": "2026-07",
"plan": "free",
"used": {"writes": 12, "reads": 340, "memories": 87, "chats": 3},
"limits": {"writes": 100, "reads": 2500, "memories": 1000, "chats": 25},
"exceeded": false
}
Counters are month-keyed, so a new month starts from zero with no reset to wait for
(memories is the one exception — it is a level, not a flow, so it is not scoped to a
period). Exceeding a meter on Free or pay-as-you-go returns 429 quota_exceeded on the
affected route; a subscribed plan overflows into the wallet instead — see
Overage.
Multi-tenant SaaS: one account or many?
If you are building a product whose customers each have many end users, the natural shape is your GitLoom account : your customers : their users = 1 : n : m, with one namespace per end user and a naming convention that encodes both levels:
c42-u8213
c42-u9007
c91-u1004
A single account means one credential to manage and one quota to watch. It also means one blast radius: a leaked key exposes every customer. Separate GitLoom accounts per customer give a real credential boundary, at the cost of managing signups and quotas per customer — and there is no API for provisioning accounts today, so that path is manual. For most launches, one account with a strict server-side namespace derivation is the right call.