memcity

Enterprise

Access Control

Secure multi-tenant knowledge bases with per-document ACLs using principal-based filtering.

What are ACLs?

ACL stands for Access Control List. It's a list of "who is allowed to see this document."

If you're building a SaaS application where different customers have different data, or an internal tool where HR documents shouldn't be visible to engineering, you need ACLs. Without them, every search returns results from every document — even ones the user shouldn't see.

Memcity's ACL system is simple but powerful: each document can have a list of principals who can access it. When a user searches, results are automatically filtered to only include documents they're allowed to see.

Key Concepts

Principals

A principal is an identifier that represents who has access. Memcity supports three types:

TypeFormatExampleUse Case
Useruser:aliceA specific personPersonal documents, draft content
Rolerole:adminA role in your systemAdmin-only settings, management docs
Groupgroup:engineeringA team or departmentDepartment-specific knowledge bases

You define what principals your users have — Memcity just stores and matches them.

Default Behavior: No ACL = Public

If a document has no ACL set, it's visible to everyone. This means you can gradually add ACLs — existing documents remain public until you explicitly restrict them.

typescript
Document A: ACL = ["user:alice", "role:admin"]Only Alice and admins can see it
Document B: ACL = ["group:engineering"]Only the engineering group can see it
Document C: ACL = (not set)Everyone can see it

How It Works

Enabling ACLs

ACLs are a Team-tier feature. Enable them in your config:

ts
const memory = new Memory(components.memcity, {
  tier: "team",
  enterprise: {
    acl: true,  // Enable per-document access control
  },
});

Setting ACLs on Documents

After ingesting a document, set its ACL:

ts
// Ingest a confidential HR document
await memory.ingestText(ctx, {
  orgId,
  knowledgeBaseId: kbId,
  text: "Employee salary bands: Junior $60-80k, Senior $100-130k...",
  source: "salary-bands.md",
});
 
// Restrict access to HR team and admins only
await memory.setDocumentAcl(ctx, {
  orgId,
  documentId: docId,
  principals: ["group:hr", "role:admin"],
});

You can also set ACLs at ingestion time:

ts
await memory.ingestText(ctx, {
  orgId,
  knowledgeBaseId: kbId,
  text: "Q4 revenue numbers: $12.5M...",
  source: "financials-q4.md",
  principals: ["group:finance", "group:executive", "role:admin"],
});

Searching with ACLs

When ACLs are enabled, pass the user's principals in the search request:

ts
// Alice is in the engineering group and has a developer role
const results = await memory.getContext(ctx, {
  orgId,
  knowledgeBaseId: kbId,
  query: "What are the salary bands?",
  principals: ["user:alice", "role:developer", "group:engineering"],
});
 
// Results will NOT include the salary bands document
// because Alice's principals don't overlap with ["group:hr", "role:admin"]
ts
// Bob is an HR manager
const results = await memory.getContext(ctx, {
  orgId,
  knowledgeBaseId: kbId,
  query: "What are the salary bands?",
  principals: ["user:bob", "role:manager", "group:hr"],
});
 
// Results WILL include the salary bands document
// because "group:hr" is in both Bob's principals and the document's ACL

Multi-Principal Matching

A user can have multiple principals. They get access to any document that matches any of their principals:

ts
// Carol has three principals
const principals = ["user:carol", "role:admin", "group:finance"];
 
// She can see:
// - Documents with ACL containing "user:carol"
// - Documents with ACL containing "role:admin"
// - Documents with ACL containing "group:finance"
// - Documents with no ACL (public)

Updating ACLs

You can update a document's ACL at any time:

ts
// Add engineering access to a document
await memory.setDocumentAcl(ctx, {
  orgId,
  documentId: docId,
  principals: ["group:hr", "group:engineering", "role:admin"],
});
 
// Remove all ACLs (make it public again)
await memory.setDocumentAcl(ctx, {
  orgId,
  documentId: docId,
  principals: [],  // Empty array = no ACL = public
});

How ACLs Integrate with the Search Pipeline

ACL filtering is Step 9 in the 16-step pipeline. Here's where it fits:

  1. Steps 1-8: Query processing, embedding, search, fusion (operates over ALL documents)
  2. Step 9: ACL filtering — Remove results the user can't access
  3. Steps 10-16: Dedup, rerank, expand, score, format (operates on filtered results)

Why filter after search, not during?

Filtering during vector search would require per-user indexes, which is impractical. Instead, Memcity searches the full index for best recall, then filters the results. Since search typically returns 50-100 candidates and filtering is an O(n) operation, the performance impact is negligible (under 5ms).

Best Practices

Consistent Principal Naming

Pick a naming convention and stick with it:

typescript
user:{your_app_user_id}user:clerk_abc123
role:{role_name}role:admin, role:editor, role:viewer
group:{department_or_team}group:engineering, group:hr, group:executive

Combine ACLs with Audit Logging

Enable both enterprise.acl and enterprise.auditLog together. The audit log records every access-denied event, which is valuable for security monitoring:

ts
enterprise: {
  acl: true,
  auditLog: true,  // Logs who was denied access to what
}

Don't Over-Restrict

Start with broad access and narrow down. It's better to have slightly too-broad access during development than to have users unable to find documents because ACLs are too restrictive.

Per-Tenant Knowledge Bases

For SaaS applications with strong data isolation requirements, consider using separate knowledge bases per tenant in addition to ACLs. This provides two layers of isolation:

ts
// Tenant A's knowledge base
const kbA = await memory.createKnowledgeBase(ctx, {
  orgId,
  name: "Tenant A Docs",
});
 
// Tenant B's knowledge base
const kbB = await memory.createKnowledgeBase(ctx, {
  orgId,
  name: "Tenant B Docs",
});
 
// Search is already scoped to a specific KB
// ACLs provide additional document-level control within the KB

Availability

Access Control is available on the Team tier only ($179 one-time).