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

<ParamField path="_acl.owner" type="string" required>
  The `principal_id` of the user who created the document. Only the owner (or an org-scoped key) can modify ACL settings.
</ParamField>

<ParamField path="_acl.read" type="string[]" required>
  List of `principal_id` values that can read this document.
</ParamField>

<ParamField path="_acl.write" type="string[]" required>
  List of `principal_id` values that can update this document.
</ParamField>

<ParamField path="_acl.public" type="boolean" required>
  When `true`, the document is visible to all user-scoped keys regardless of the `read` list.
</ParamField>

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

<CodeGroup>
  ```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 '{
      "principal_id": "user_123",
      "description": "Key for user 123"
    }'
  ```
</CodeGroup>

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

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

<CodeGroup>
  ```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
    }'
  ```
</CodeGroup>

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

<Steps>
  <Step title="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`.
  </Step>

  <Step title="Users ingest documents with their key">
    Each document is automatically tagged with the user's ACL. User A cannot see User B's documents.
  </Step>

  <Step title="Users search normally">
    Retriever executions with a user-scoped key only return documents the user can access — no extra filters needed.
  </Step>

  <Step title="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.
  </Step>
</Steps>

### Public Knowledge Base with Private Uploads

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

<CodeGroup>
  ```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
  ```
</CodeGroup>

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

To backfill ACL on existing documents, use a bulk update with an org-scoped key:

```bash theme={null}
curl -X POST https://api.mixpeek.com/v1/collections/col_abc123/documents/bulk-update \
  -H "Authorization: Bearer mxp_sk_org-key" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {},
    "update": {
      "_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>

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

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

## Related

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