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:
| Type | Format | Example | Use Case |
|---|---|---|---|
| User | user:alice | A specific person | Personal documents, draft content |
| Role | role:admin | A role in your system | Admin-only settings, management docs |
| Group | group:engineering | A team or department | Department-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.
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 itHow It Works
Enabling ACLs
ACLs are a Team-tier feature. Enable them in your config:
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:
// 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:
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:
// 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"]// 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 ACLMulti-Principal Matching
A user can have multiple principals. They get access to any document that matches any of their principals:
// 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:
// 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:
- Steps 1-8: Query processing, embedding, search, fusion (operates over ALL documents)
- Step 9: ACL filtering — Remove results the user can't access
- 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:
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:executiveCombine 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:
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:
// 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 KBAvailability
Access Control is available on the Team tier only ($179 one-time).