One Brain per customer:
multi-tenant AI memory with scoped keys
Most AI features don't fail on the model — they fail on memory. An assistant that starts from zero every session is glorified autocomplete. It gets interesting when it remembers — and, for a SaaS product, when it remembers separately for each customer. Here is how we built exactly that on cachly.
The problem: one tenant, many customers
We built this for Kanzlei-Kompass, an OKR platform for tax-advisory firms. Every firm gets its own AI coach that learns from that firm'spast analyses and decisions. No shared pool, no mixed data. But Kanzlei-Kompass is a single SaaS app: one operator, many firms. For AI memory that means firm A's knowledge must never surface in firm B's coach. Two naive approaches:
- One shared brain, separated by a filter. One bug in the filter, one forgotten
WHERE tenant = …, and data leaks. For the client confidentiality of tax firms, that's a non-starter. - One cachly account per firm. Cleanly isolated, but a billing and provisioning nightmare at hundreds of customers.
We wanted the isolation of the second with the operability of the first. The answer: one tenant, many instances, and a per-customer key scoped to a single instance.
The building blocks in cachly
cachly has three layers: a tenant (the account), an instance (an isolated brain with its own datastore), and an API key. For the multi-tenant case we added two capabilities.
1. A per-tenant instance limit. An app tenant that provisions one instance per end customer needs headroom. The instance limit is now overridable per tenant, so "one firm = one instance" scales cleanly without loosening the default for everyone else.
2. Instance-scoped API keys. This is the heart of it. A key can optionally bind to exactly one instance:
POST /api/v1/api-keys
Authorization: Bearer <tenant-admin-key>
{ "name": "kanzlei-mueller", "scope": "read_write",
"instance_id": "…this firm's instance…" }A key bound this way may touch only that one instance. Any attempt to hit a different instance — or a tenant-wide route — is rejected server-side with a 403: deny-by-default. The guard reads the instance id from the path (the segment after instances), independent of the concrete route:
POST /api/v1/instances/<bound-id>/recall OK
POST /api/v1/instances/<other-id>/recall 403
POST /api/v1/instances 403 (tenant-wide)
GET /api/v1/api-keys 403 (tenant-wide)One detail from practice that cost us time: the guard runs as group middleware — and in Fiber, a downstream route's params are not populated inside Use middleware. A c.Params("id")-based guard would have blocked every scoped-key request, including the legitimate ones. So the guard parses the id from the raw path instead. A table test pins that down: JWT user, tenant-wide key, matching vs. foreign instance, and the org-route form.
The trust model: two keys, two roles
From these blocks comes a clean split that confines a leaked credential to exactly one customer:
- The tenant-admin key lives only on the app server, never at a client. It is used once per customer, to createthat customer's instance and its scoped key. Because it can reach every instance, it never leaves the provisioning path.
- The instance-scoped key is stored (encrypted) per customer in the app database and used for day-to-day
recall/learn. Even in the worst case it can only reach the one brain it belongs to.
The lifecycle of a customer
- Provision (idempotent, at login). On a firm's first login the app creates a free instance, waits briefly for
running, mints aread_writekey scoped to that instance, and stores it AES-GCM encrypted. The write only lands while no instance is recorded yet, so parallel logins can't create a second brain. - Recall (before every answer). The coach calls
recalland folds the firm's experience into the prompt. Finds nothing? It just answers normally. - Learn (after analyses). From every OKR analysis the coach writes a lesson back via
learn. The memory grows with every cycle.
Fail-soft is not a nice-to-have
One principle runs through all of it: no brain outage may block a user path. Every call has a hard timeout. If the brain is down, recall returns an empty list, learn evaporates with a log line, provisioning is retried at the next login. The coach then answers without memory context — but it answers.
Why this matters beyond one app
The pattern isn't firm-specific. Any multi-tenant app that wants an isolated AI memory per customer — agencies, practices, consultancies, portfolios — can use the same three layers: one tenant, one instance per customer, one scoped key per instance. The isolation lives in a server-side guard, not in a filter line you can forget.
cachly is a persistent AI Brain for developers — memory shared across Claude Code, Cursor, GitHub Copilot & Windsurf simultaneously. Auto-detects every editor. Bootstraps from your git history. 126 MCP tools. Free tier, EU servers, no credit card.
Your AI is forgetting everything right now.
Every session starts blank. Every bug re-discovered. Every deploy procedure re-explained. cachly fixes that in 30 seconds — your AI remembers every lesson, every fix, every teammate's hard-won knowledge. Forever.