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

# Document-Level ACL

> Row-level security for multi-user applications with automatic access control on documents

## Overview

Document-level ACL (Access Control Lists) lets you build multi-user applications on top of Mixpeek where each end-user only sees the documents they are authorized to access. Instead of managing separate namespaces per user, you store all documents in a single namespace and let Mixpeek enforce read/write permissions automatically.

Key capabilities:

* **Automatic filter injection** — user-scoped API keys transparently filter all reads so users only see their own (or shared) documents
* **Per-document ownership** — every document has an `_acl` object tracking who can read, write, or own it
* **Public documents** — mark a document as `public: true` to make it visible to all users
* **Zero application-side filtering** — your app does not need to add user filters to queries; Mixpeek handles it server-side

## Concepts

### API Key Types

Mixpeek supports two types of API keys:

| Key Type | Prefix | ACL Behavior |
| - | - | - |
| **Org-scoped** | `mxp_sk_` | Full access — bypasses ACL filters entirely |
| **User-scoped** | `usr_sk_` | Restricted — ACL filters injected on every read and write |

Org-scoped keys are for your backend services and admin operations. User-scoped keys are for end-user sessions in your application.

### The `_acl` Object

Every document stores an `_acl` sub-object inside its `_internal` payload:

```json theme={null}
{
  "_internal": {
    "_acl": {
      "owner": "user_123",
      "read": ["user_123", "user_456"],
      "write": ["user_123"],
      "public": false
    }
  }
}
```

**\_acl.owner** `string` *required*

The `principal_id` of the user who created the document. Only the owner (or an org-scoped key) can modify ACL settings.

**\_acl.read** `string[]` *required*

List of `principal_id` values that can read this document.

**\_acl.write** `string[]` *required*

List of `principal_id` values that can update this document.

**\_acl.public** `boolean` *required*

When `true`, the document is visible to all user-scoped keys regardless of the `read` list.

## Creating User-Scoped API Keys

Generate a user-scoped key by providing a `principal_id` that represents the end-user in your application (e.g., your internal user ID, email, or UUID).

```python Python theme={null}
from mixpeek import Mixpeek

# Use your org-scoped key to create a user-scoped key
client = Mixpeek(api_key="mxp_sk_your-org-key")

user_key = client.organizations.api_keys.create(
    principal_id="user_123",  # setting principal_id makes the key user-scoped
    description="Key for user 123"
)

print(user_key.api_key)  # usr_sk_...
```

```javascript JavaScript theme={null}
import { Mixpeek } from 'mixpeek-sdk'

const client = new Mixpeek({ apiKey: 'mxp_sk_your-org-key' })

const userKey = await client.organizations.apiKeys.create({
  principalId: 'user_123', // setting principalId makes the key user-scoped
  description: 'Key for user 123'
})

console.log(userKey.apiKey) // usr_sk_...
```

```bash cURL theme={null}
curl -X POST https://api.mixpeek.com/v1/organizations/users/{user_email}/api-keys \
  -H "Authorization: Bearer mxp_sk_your-org-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "user-123-key",
    "principal_id": "user_123",
    "description": "Key for user 123"
  }'
```

> [!NOTE]
>
> Store the returned `usr_sk_` key securely. Pass it to your frontend or mobile app so the end-user's requests are automatically scoped.

## How ACL Works

### Automatic ACL on Document Creation

When a user-scoped key creates a document, Mixpeek automatically sets the `_acl`:

* `owner` = the key's `principal_id`
* `read` = `[principal_id]`
* `write` = `[principal_id]`
* `public` = `false`

You do not need to set `_acl` manually — it is populated from the API key's identity.

### Automatic Filter Injection on Reads

Every read operation made with a user-scoped key — including retriever executions, document listings, and searches — has an ACL filter injected server-side. The filter ensures the user only sees documents where:

1. Their `principal_id` is in the `_acl.read` list, **OR**
2. The document has `_acl.public: true`

This happens transparently. Your application code does not need to add any user-specific filters.

### Write Protection

Update and delete operations made with a user-scoped key check the `_acl.write` list. If the user's `principal_id` is not in the write list, the operation returns a `403 Forbidden` error.

### Org-Scoped Key Bypass

Org-scoped keys (`mxp_sk_` prefix) bypass all ACL checks. They can read, write, and modify any document regardless of its `_acl` settings. Use org-scoped keys for backend services, admin dashboards, and data pipelines.

## Managing ACL

Update a document's ACL using the dedicated endpoint. Only the document owner or an org-scoped key can modify ACL settings.

```python Python theme={null}
# Share a document with another user (add read access)
client = Mixpeek(api_key="usr_sk_owner-key")

client.collections.documents.update_acl(
    collection_id="col_abc123",
    document_id="doc_xyz789",
    read=["user_123", "user_456"],  # Add user_456 to readers
)
```

```javascript JavaScript theme={null}
const client = new Mixpeek({ apiKey: 'usr_sk_owner-key' })

await client.collections.documents.updateAcl({
  collectionId: 'col_abc123',
  documentId: 'doc_xyz789',
  read: ['user_123', 'user_456'] // Add user_456 to readers
})
```

```bash cURL theme={null}
curl -X PATCH https://api.mixpeek.com/v1/collections/col_abc123/documents/doc_xyz789/acl \
  -H "Authorization: Bearer usr_sk_owner-key" \
  -H "Content-Type: application/json" \
  -d '{
    "read": ["user_123", "user_456"],
    "write": ["user_123"],
    "public": false
  }'
```

### Making a Document Public

Set `public: true` to make a document visible to all user-scoped keys in the namespace:

```bash theme={null}
curl -X PATCH https://api.mixpeek.com/v1/collections/col_abc123/documents/doc_xyz789/acl \
  -H "Authorization: Bearer usr_sk_owner-key" \
  -H "Content-Type: application/json" \
  -d '{
    "public": true
  }'
```

### Revoking Access

Remove a user from the `read` or `write` list by sending the updated list without their `principal_id`:

```bash theme={null}
# Remove user_456 from readers — only user_123 retains access
curl -X PATCH https://api.mixpeek.com/v1/collections/col_abc123/documents/doc_xyz789/acl \
  -H "Authorization: Bearer usr_sk_owner-key" \
  -H "Content-Type: application/json" \
  -d '{
    "read": ["user_123"],
    "write": ["user_123"]
  }'
```

## Examples

### Multi-User Document Search

A SaaS app where each user uploads and searches their own documents, with optional sharing.

**1. Create user-scoped keys for each user**

When a user signs up, generate a `usr_sk_` key with their user ID as the `principal_id`.

**2. Users ingest documents with their key**

Each document is automatically tagged with the user's ACL. User A cannot see User B's documents.

**3. Users search normally**

Retriever executions with a user-scoped key only return documents the user can access — no extra filters needed.

**4. Share a document**

User A shares a document with User B by adding User B's `principal_id` to the `read` list via the ACL endpoint.

### Public Knowledge Base with Private Uploads

Combine public reference documents with private user uploads in the same namespace:

```python Python theme={null}
# Admin creates public reference docs using org-scoped key
admin_client = Mixpeek(api_key="mxp_sk_org-key")

# Create a document and make it public
doc = admin_client.collections.documents.create(
    collection_id="col_kb",
    data={"title": "Getting Started Guide", "content": "..."}
)

admin_client.collections.documents.update_acl(
    collection_id="col_kb",
    document_id=doc.document_id,
    public=True,
)

# End-user creates private notes using their user-scoped key
user_client = Mixpeek(api_key="usr_sk_user-key")

user_client.collections.documents.create(
    collection_id="col_kb",
    data={"title": "My private notes", "content": "..."}
)
# _acl automatically set: owner=user, read=[user], write=[user], public=false
```

When the end-user searches, they see both the public reference docs and their own private notes — but not other users' private documents.

## Backwards Compatibility

Documents created before ACL was enabled do not have an `_acl` field. These documents follow these rules:

| Key Type | Behavior for Documents Without `_acl` |
| - | - |
| **Org-scoped** | Full access — documents are visible and writable as before |
| **User-scoped** | **Not visible** — documents without `_acl` are excluded from reads |

> [!WARNING]
>
> If you adopt user-scoped keys on an existing namespace, pre-existing documents will be invisible to end-users until you set their `_acl` (either individually or via a bulk update). Org-scoped keys continue to work normally.

To backfill ACL on existing documents, use a bulk update with an org-scoped key. Omitting
`filters` applies the update to every document in the collection:

```bash theme={null}
curl -X PATCH https://api.mixpeek.com/v1/collections/col_abc123/documents/bulk \
  -H "Authorization: Bearer mxp_sk_org-key" \
  -H "X-Namespace: $MP_NAMESPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "update_data": {
      "_internal._acl": {
        "owner": "system",
        "read": [],
        "write": [],
        "public": true
      }
    }
  }'
```

> [!WARNING]
>
> **This returns before the backfill has run.** The response carries `task_id` and
> `status: "PENDING"`, and `updated_count` is `null`. Poll `GET /v1/tasks/{task_id}` until
> it reports a terminal status, and only then treat the documents as visible to
> user-scoped keys.

To scope the backfill instead of applying it to everything, send `filters` in the
`{field, operator, value}` shape:

```json theme={null}
{
  "filters": {"AND": [{"field": "metadata.status", "operator": "eq", "value": "pending"}]},
  "update_data": {"_internal._acl": {"owner": "system", "read": [], "write": [], "public": true}}
}
```

## Best Practices

> [!TIP]
>
> Use your own stable user identifiers (UUIDs, database IDs) as `principal_id` values. Avoid using email addresses, which can change.

> [!TIP]
>
> Keep org-scoped keys on your backend only. Never expose `mxp_sk_` keys to client-side code. User-scoped `usr_sk_` keys are safe to use in frontend and mobile apps.

> [!WARNING]
>
> ACL is enforced at the Mixpeek API layer. If you have direct access to the underlying MVS or MongoDB instances, ACL filters are not applied. Always route end-user traffic through the Mixpeek API.

## Related

* [Security & Tenancy](https://docs.mixpeek.com/docs/operations/security.md) — org-level authentication and namespace isolation
* [Filters](https://docs.mixpeek.com/docs/retrieval/filters.md) — manual query filters (ACL filters are injected automatically on top of these)
* [Namespaces](https://docs.mixpeek.com/docs/ingestion/namespaces.md) — data isolation boundaries


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.