Skip to main content
Memvid supports permission-aware retrieval by storing ACL metadata on every frame (chunk) and enforcing it inside retrieval (search/ask) in memvid-core. This unlocks:
  • Strict multi-tenant isolation (no cross-tenant leakage)
  • RBAC (roles/groups/principals) at frame/chunk level
  • A single .mv2 per environment with metadata-based enforcement (recommended)

Mental Model

  • A frame is the atomic unit of retrieval in Memvid. When you ingest a PDF, Memvid creates many frames (chunks).
  • ACL is evaluated per frame, so “chunk-level ACL” means “frame-level ACL metadata”.
  • ACL is not keyword-based: it does not guess who can see content. You decide the policy at ingest time.

ACL Metadata (Ingest-Time)

Attach the following keys in the frame metadata (stored on disk in the frame’s extra_metadata):
KeyTypeRequiredMeaning
acl_tenant_idstringYes (recommended)Tenant boundary for strict isolation
acl_visibility"public" | "restricted"Yespublic is readable by anyone in-tenant; restricted requires a match
acl_read_rolesstring[]If restrictedAllowed roles
acl_read_groupsstring[]If restrictedAllowed group IDs
acl_read_principalsstring[]If restrictedAllowed subject/principal IDs
acl_policy_versionstringYesPolicy schema version (currently "v1")
acl_resource_idstringOptionalStable lineage identifier (optional)
In Node/Python SDKs you can provide string[] values directly for acl_read_*. The SDK will normalize and persist them in a canonical form for the core evaluator.ACL strings are normalized (trimmed + lowercased). Treat role/group/principal identifiers as case-insensitive.

Example: Role-Restricted Chunk

{
  "acl_tenant_id": "allera-prod",
  "acl_visibility": "restricted",
  "acl_read_roles": ["finance"],
  "acl_policy_version": "v1"
}
If acl_visibility is "restricted" and you provide no acl_read_roles / acl_read_groups / acl_read_principals, the chunk will be denied for everyone in enforce mode.

ACL Context (Query-Time)

At query time you provide the caller identity via acl_context / aclContext:
FieldTypeMeaning
tenant_id / tenantIdstringTenant boundary (required for enforcement)
subject_id / subjectIdstringThe current user (principal)
rolesstring[]User roles
group_ids / groupIdsstring[]User group IDs
And choose an enforcement mode:
  • audit: evaluate ACL but do not block results (migration/testing)
  • enforce: filter results; deny-by-default for missing/invalid ACL metadata
Do not accept acl_context from untrusted clients. Build it server-side from your auth system (JWT claims, your RBAC store, etc.) so users cannot self-assign roles.

Creating an ACL-Scoped API Key (Dashboard)

In the Memvid dashboard:
  1. Go to API Keys and click Create Key
  2. Enable ACL scope
  3. Set Tenant ID (required for strict isolation)
  4. Optionally set Roles, Group IDs, and Subject ID
  5. Choose enforcement mode: audit or enforce
For most apps, keep the API key as a server-side credential and compute the end-user acl_context from your auth system on every request.

End-to-End (Node.js)

import {
  configure, create,
  getAclScopeFromApiKey, aclContextFromScope, aclMetadataFromScope,
} from "@memvid/sdk";

configure({ apiKey: process.env.MEMVID_API_KEY, dashboardUrl: "https://memvid.com" });

const scope = await getAclScopeFromApiKey();          // reads /api/ticket (control plane)
const aclContext = aclContextFromScope(scope);        // { tenantId, subjectId?, roles?, groupIds? }
const aclMeta = aclMetadataFromScope(scope, { visibility: "restricted" });

const mv = await create("kb.mv2", "basic", { enableLex: true, enableVec: true });

await mv.put({ title: "Finance doc", label: "kb", text: "Q4 budget...", metadata: aclMeta });

const hits = await mv.find("budget", {
  mode: "lex",
  k: 5,
  aclContext,
  aclEnforcementMode: "enforce",
});

await mv.seal();

End-to-End (Python)

import os
from memvid_sdk import (
    configure, create,
    get_acl_scope_from_api_key, acl_context_from_scope, acl_metadata_from_scope,
)

configure({"api_key": os.environ["MEMVID_API_KEY"], "dashboard_url": "https://memvid.com"})

scope = get_acl_scope_from_api_key()
acl_context = acl_context_from_scope(scope)            # {"tenant_id", "subject_id"?, "roles"?, "group_ids"?}
acl_meta = acl_metadata_from_scope(scope, visibility="restricted")

mv = create("kb.mv2", enable_lex=True, enable_vec=True)
mv.put(title="Finance doc", label="kb", metadata=acl_meta, text="Q4 budget...")

hits = mv.find("budget", mode="lex", k=5, acl_context=acl_context, acl_enforcement_mode="enforce")

mv.seal()

Chunk-Level Guarding (Example Policy)

You decide which chunks are restricted to which readers at ingest time. Example policy:
  • Pages 1-20: role=finance
  • Pages 21-30: role=hr
  • Pages 31+: principal=matt
To implement this, you ingest via put_many() / putMany() with per-chunk metadata (rather than a single put_file() metadata applied to all chunks).
If you use put_file(...) with metadata=..., the same ACL metadata is applied to every produced chunk. That’s perfect for document-level ACL, but not enough for section/page-level ACL.

Single .mv2 vs One .mv2 Per Tenant

Single .mv2 per environment (recommended):
  • Store all tenants in one file
  • Always set acl_tenant_id on every frame
  • Always pass acl_context.tenant_id at retrieval
  • Use restricted + allow-lists for sensitive frames
One .mv2 per tenant (simpler operations, more files):
  • Easier isolation boundaries
  • More operational overhead (more files to manage, ticketing/capacity per file)