Mnemosyne is multi-tenant by construction: one Postgres database holds many workspaces, and a tenant must never see another tenant’s memory. This document describes the isolation model, how it fails closed, and the other safeguards (API-key hashing, PII handling) that ship in the box.
Multi-tenant isolation: RLS + FORCE, Pattern A
Every mnemo_* table enables row-level security with FORCE. FORCE
matters because, without it, the table owner bypasses RLS — FORCE applies the
policies even to the owning role. Each table’s policy (“Pattern A”) gates row
visibility on a per-transaction GUC, app.workspace_id:
- A row is visible only when its
workspace_idequalscurrent_setting('app.workspace_id'). - When the GUC is unset, the policy evaluates to no match — it fails closed, returning zero rows rather than leaking everything. A query that forgets to scope by workspace gets nothing, not the whole table.
The RLS foundation (the helper functions apply_pattern_a(),
current_workspace_id(), is_cross_tenant_admin()) is established in migration
0015_mnemo_rls_foundation.sql, which every later mnemo_* migration depends
on.
The app_user role downgrade in withMnemoTx
RLS+FORCE on a table is necessary but not sufficient. A real-world deployment
often connects as a Postgres role with BYPASSRLS (a superuser, or a managed
role with that attribute). With BYPASSRLS, Pattern A policies are silently
skipped — the isolation is theatre on the wire even though it’s real on the
table. A single missing set_config, or any future bug, would then leak rows
across tenants instead of returning zero.
withMnemoTx closes this gap. Inside one transaction, before any tenant query
runs, it:
await tx.execute(sql`SET LOCAL ROLE app_user`);
await tx.execute(sql`SELECT set_config('app.workspace_id', workspaceId, true)`);SET LOCAL ROLE app_userdowngrades the transaction toapp_user— a role defined in migration0007_postgres_roles.sqlasNOINHERIT LOGINwith noBYPASSRLS. This works even when the underlying connection is a superuser:SET ROLEto a non-BYPASSRLSrole applies for the duration of the transaction. Because it’sLOCAL, it auto-reverts on commit/rollback, so the next reservation of the same pooled connection starts clean.set_config('app.workspace_id', …, true)sets the workspace GUCLOCALto the transaction (the third arg,true, scopes it to the transaction), so it too releases on commit/rollback and never bleeds across pooled requests.
This is layer 1 of defense-in-depth: even a superuser connection cannot leak
across tenants once a query goes through withMnemoTx. Layer 2 is connecting
production directly as app_user so the elevated role is never in play to begin
with.
Optional per-actor isolation
For workspaces that need per-end-user privacy within a tenant, the
MnemoTxOptions form of withMnemoTx sets app.actor_id and flips the
app.enforce_actor_isolation GUC. When enabled, the SELECT policy on
mnemo_fact restricts visible rows to actor_id IS NULL OR actor_id = $actorId.
It defaults to off to preserve existing read scope, and an empty-string actor id
is treated as unset so a sentinel can never accidentally match a real id.
This is integration-tested
The isolation is verified end-to-end against a live pgvector container, not just
asserted in policy SQL. The integration suite confirms that a second workspace
(ws_b) sees zero of the first workspace’s (ws_a) facts, and that
cross-tenant writes are rejected — the RLS isolation holds for both reads and
writes.
API keys: Argon2id-hashed
Workspace API keys (used by @mnemosyne/server and the typed SDK to authenticate
and pin a request to a workspace) are stored as Argon2id hashes by default
(bcrypt is available as an option), never in plaintext. Verification runs the
presented key through the configured hash against the stored value, so a database
dump does not expose usable credentials. The key both authenticates the caller
and selects the workspace, which is why the SDK never transmits a workspace id
over the wire — it can’t be spoofed by the client. Keys are mintable and
rotatable via the create-api-key script.
PII detection and redaction
Mnemosyne ships a regex-based PII layer (@mnemosyne/core →
detectPII / redactPII) so sensitive strings can be caught before they’re
persisted as memory. It covers these categories, each with a severity weight:
| Category | Severity |
|---|---|
api_key | 1.00 |
credit_card | 0.95 |
ssn | 0.95 |
url_with_token | 0.85 |
phone | 0.55 |
email | 0.50 |
ip_address | 0.30 |
detectPII(text)returns{ detected, categories, risk_score, matches }, whererisk_scoreis the max severity among matched categories.redactPII(text)replaces every match with[REDACTED-<category>](global replace — all occurrences, not just the first).redactPIIWithCategoriesadditionally returns the matched categories so a caller (e.g.createFact’s PII wiring) can stash them in metadata without re-running detection.
The patterns are deliberately conservative — they prefer false negatives over false positives, on the assumption that an optional downstream NER/LLM layer catches the high-confidence cases the regexes intentionally skip. Treat the regex layer as a first line of defense, not a compliance guarantee.
Summary of the layered model
- Table-level: RLS +
FORCE+ Pattern A policy, failing closed when the workspace GUC is unset. - Transaction-level:
withMnemoTxdowngrades to the non-BYPASSRLSapp_userrole and sets the workspace GUCLOCAL, so isolation holds even on a superuser connection. - Deployment-level: connect production as
app_userso the elevated role is never on the wire. - Credential-level: Argon2id-hashed API keys (bcrypt optional).
- Content-level: PII detection + redaction before persistence.
Other safeguards in the box
This page covers isolation, key hashing, and PII handling in depth. Several more security controls ship and are worth knowing exist (each has its own dedicated documentation):
- mTLS boundary for service-to-service and federation peer traffic.
- RBAC with 15 fine-grained scopes (e.g.
memory:read,memory:write,memory:admin:cross_tenant). - Break-glass access requiring two-person approval for privileged actions.
- Merkle tamper-evident audit log — append-only, hash-chained.
- IP allowlist to restrict which networks may reach the API.
- Region-lock / data residency via
MNEMO_REGION.