Each end user's memory is a file of its own.
Isolation by construction, erasure you can count, keys kept as digests and logs without bodies. What is not built yet is marked, not implied.
- IsolationOne database file per end userLive
- AccountFrom the key and a header, never a bodyLive
- ErasureEvery copy, with a receipt and a tombstoneLive
- API keysSHA-256 digests; the token is shown onceLive
- Request logsMetadata only, kept 7 daysLive
- Encryption at restWith customer-managed keysSoon
- SOC 2 Type IIAn independent audit of these controlsSoon
Isolation is a file, and a column
Every end user's memory is a separate SQLite database. One user's writes are never in the same file as another's, so a query cannot reach across by mistake.
Inside each file, the tenant is still the first column of every key, and no statement reads a table without it: a second guard, should a statement ever reach the wrong file.
The account comes from your key and the X-Twin-Account header, never from a request body, so a model calling a tool cannot name another user.
| Key | X-Twin-Account | Memory |
|---|---|---|
| production key | user_42 | acme.production.user_42 |
| production key | user_43 | acme.production.user_43 |
| a staging key | user_42 | acme.staging.user_42 |
Erasure with a receipt
Forget hard-deletes every copy: the item as sent, the passages cut from it, the records read from it, their index rows and vectors, and any copy still waiting in the queue. The response counts each table before and after.
The erased id is kept as a tombstone, so a retry still in flight cannot bring it back. An item with an expiry is erased the same way, on time.
| Table | Before | After |
|---|---|---|
| events | 4 | 3 |
| threads | 3 | 2 |
| passages | 3 | 2 |
| mentions | 13 | 11 |
| passages_fts | 3 | 2 |
| files | 14 | 15 |
Keys are stored as digests
A key's token is shown once, when it is made. We keep its SHA-256 digest and its first ten characters, so the console can show which key is which without being able to show the key.
Revoking deletes the row. A read key cannot write, and an MCP client given one sees no write tool.
{
"id": "key_3f9a1c2b7d4e",
"prefix": "tm_AbC1234",
"sha256": "9c1e5b7a…",
"org": "o7k2m9qx4ab",
"project": "production",
"label": "backend",
"writes": false,
"created": "2026-10-21T09:14:03Z"
}Request logs hold metadata, for 7 days
Each request leaves one line: when, which route, which project and end user, the status, the time it took and what it spent. No query, no item text, no response body. Lines are deleted after 7 days.
{
"at": "2026-10-21T09:15:42Z",
"request_id": "req_6b1f0e9d2a7c",
"route": "/v1/memory/search",
"project": "production",
"user": "user_42",
"status": 200,
"ms": 184,
"key_id": "key_3f9a1c2b7d4e",
"model_calls": 1,
"usd": 0.0004
}Which model providers see text
Two steps send text to a model: reading an item into records, once, and filtering a search down to what the query needs. Both go through OpenRouter. Everything else runs on our servers.
The service keeps no copy of a model's reply, since a reply holds your users' words.
| Step | Sends |
|---|---|
| Reading items into records | The item's text |
| Filtering a search | The query and candidate passages |
| Landing, indexing, embedding, ranking | Nothing |
Reporting a vulnerability
Email security@twinmemory.dev with what you found and how to reproduce it.
- Test only against accounts and keys you own.
- Read no more of anyone else's data than it takes to show the issue, and tell us what you saw.
- Give us a reasonable time to fix it before you publish.
- Leave out denial of service, spam and social engineering of our staff.