RecipesMulti-tenant SaaS

Multi-tenant SaaS

If you build SaaS, you’ve solved this problem before in your own app: “how do I keep customer A’s data away from customer B?”. Mnemosyne inherits the most rigorous answer there is — PostgreSQL Row-Level Security with FORCE.

The setup

const mnemo = new MnemosyneClient({
  baseUrl: "http://localhost:3000",
  apiKey: process.env.MNEMO_KEY!,
  // workspaceId is determined per-request from the authenticated user
});
 
app.get("/recall", auth, async (req, res) => {
  const result = await mnemo.recall({
    workspaceId: `tenant:${req.user.tenantId}`,  // ← the key line
    query: req.query.q,
  });
  res.json(result);
});

What you don’t have to write

You don’t write WHERE tenant_id = $1 anywhere. The Postgres role that Mnemosyne uses has FORCE ROW LEVEL SECURITY set on every table — even the application itself can’t bypass it. Every query Mnemosyne issues runs under the GUC app.workspace_id, and RLS rewrites every read and write to enforce isolation.

-- migration 0015_mnemo_rls_foundation.sql (excerpt)
ALTER TABLE mnemo_fact ENABLE ROW LEVEL SECURITY;
ALTER TABLE mnemo_fact FORCE ROW LEVEL SECURITY;
CREATE POLICY mnemo_fact_tenant_isolation ON mnemo_fact
  USING (workspace_id = current_setting('app.workspace_id'));

If your application has a SQL injection bug and someone manages to issue SELECT * FROM mnemo_fact, they still only see the rows for the current GUC.

The actor isolation policy

Above and beyond per-workspace isolation, Mnemosyne also enforces per-agent isolation when mnemo_access_policy is configured. A support bot can be told “you can only see facts authored by support bots and facts marked tier:public” — and the database enforces that even against application bugs.

See migration 0040_mnemosyne_actor_isolation_policy for the details.

Backup isolation

Workspaces are also the unit of export and import. mnemo export dumps the workspace tied to the API key you authenticate with (gzip JSONL to stdout), so use that tenant’s key:

MNEMO_KEY=<acme-corp key> mnemo export > acme-corp.jsonl.gz
MNEMO_KEY=<restore key> mnemo import acme-corp.jsonl.gz --from mnemo --conflict-resolution skip

A customer asks for their data → one command. GDPR Article 20 (data portability) is satisfied without writing any custom code.

Cross-tenant aggregation

If you have legitimate need to look across tenants (admin dashboards, billing aggregation), Mnemosyne exposes a mnemo_org_fact_view (migration 0050) that requires a separate scope (memory:admin:cross_tenant). Without that scope, the view returns zero rows even to the application.

Multi-tenant safety is not a feature you reach for in a sprint — it’s the foundation everything else sits on. We built it first, before any cognitive feature.