> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memvid.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Permission-Aware Retrieval (ACL)

> Enforce tenant isolation and RBAC at frame/chunk level during search and RAG

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`):

| Key                   | Type                       | Required          | Meaning                                                                 |
| --------------------- | -------------------------- | ----------------- | ----------------------------------------------------------------------- |
| `acl_tenant_id`       | `string`                   | Yes (recommended) | Tenant boundary for strict isolation                                    |
| `acl_visibility`      | `"public" \| "restricted"` | Yes               | `public` is readable by anyone in-tenant; `restricted` requires a match |
| `acl_read_roles`      | `string[]`                 | If `restricted`   | Allowed roles                                                           |
| `acl_read_groups`     | `string[]`                 | If `restricted`   | Allowed group IDs                                                       |
| `acl_read_principals` | `string[]`                 | If `restricted`   | Allowed subject/principal IDs                                           |
| `acl_policy_version`  | `string`                   | Yes               | Policy schema version (currently `"v1"`)                                |
| `acl_resource_id`     | `string`                   | Optional          | Stable lineage identifier (optional)                                    |

<Info>
  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.
</Info>

### Example: Role-Restricted Chunk

```json theme={null}
{
  "acl_tenant_id": "acme-prod",
  "acl_visibility": "restricted",
  "acl_read_roles": ["finance"],
  "acl_policy_version": "v1"
}
```

<Warning>
  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.
</Warning>

***

## ACL Context (Query-Time)

At query time you provide the caller identity via `acl_context` / `aclContext`:

| Field                      | Type       | Meaning                                    |
| -------------------------- | ---------- | ------------------------------------------ |
| `tenant_id` / `tenantId`   | `string`   | Tenant boundary (required for enforcement) |
| `subject_id` / `subjectId` | `string`   | The current user (principal)               |
| `roles`                    | `string[]` | User roles                                 |
| `group_ids` / `groupIds`   | `string[]` | 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

<Warning>
  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.
</Warning>

***

## 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`

<Tip>
  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.
</Tip>

***

## End-to-End (Node.js)

```ts theme={null}
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)

```python theme={null}
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).

<Info>
  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**.
</Info>

***

## 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)
