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

# Permissions

> Authorize retrieval per end-user — with Mixpeek's built-in document ACL or your own external authorization system (OpenFGA).

## Overview

Permissions control which documents an end-user can see when they search. Mixpeek
enforces them **server-side at retrieval time**, so a user's query only ever
returns documents they are authorized to access — your application never has to
add per-user filters, and there is no way for a client to bypass them.

There are two ways to drive permissions, and they share the same enforcement
path:

<CardGroup cols={2}>
  <Card title="Built-in document ACL" href="/docs/platform/operations#document-access-control">
    Mixpeek stores and enforces an `_acl` (owner / read / write / public) on each
    document, keyed on user-scoped API keys. Best when Mixpeek is your source of
    truth for who-can-see-what.
  </Card>

  <Card title="External authorization (OpenFGA)" href="#external-authorization-openfga">
    Keep your existing permissions in your own [OpenFGA](https://openfga.dev)
    deployment; Mixpeek queries it at retrieval time and filters results to match.
    Best when an external system already owns authorization.
  </Card>
</CardGroup>

Both are **opt-in** and **fail-closed**: a document with no resolvable grant is
excluded, never leaked. Namespaces that don't opt in behave exactly as before.

## Which should I use?

|                     | Built-in document ACL                          | External authorization (OpenFGA)                               |
| ------------------- | ---------------------------------------------- | -------------------------------------------------------------- |
| **Source of truth** | Mixpeek (`_acl` on each document)              | Your OpenFGA store                                             |
| **Grant model**     | `read` list + `public` flag                    | Your full OpenFGA model (groups, roles, folder inheritance, …) |
| **Best for**        | Apps where Mixpeek owns permissions            | Apps where an external system already owns permissions         |
| **Setup**           | Create user-scoped keys; ACL set automatically | Run OpenFGA; opt the namespace in; map subjects                |
| **Consistency**     | Strong (enforced inline)                       | Strong (pull) or eventual (push) — you choose                  |

If you already manage authorization in OpenFGA (or a Zanzibar-style system), use
external authorization so you don't duplicate your permission model. Otherwise
the [built-in document ACL](/docs/platform/operations#document-access-control) is the simplest path.

## External authorization (OpenFGA)

When a namespace opts in, Mixpeek acts as a **relying party** on your OpenFGA: at
retrieval time it asks OpenFGA what the acting user can see and filters the
results accordingly. Your OpenFGA remains the single source of truth.

### How it works

1. **The acting subject** is the `principal_id` of the user-scoped API key
   executing the query, mapped to an OpenFGA subject — `user:<principal_id>` by
   default.
2. **Mixpeek queries OpenFGA** for that subject using one of the strategies below.
3. **Results are filtered** — unauthorized documents are removed before the
   response is returned, for both ad-hoc and saved retrievers.

Your OpenFGA model must represent each Mixpeek document as an object whose id is
the Mixpeek `document_id`. By default Mixpeek checks the relation `viewer` on the
object type `document`:

```
document:doc_abc123 # viewer @ user:alice      # alice can retrieve doc_abc123
document:doc_abc123 # viewer @ user:*          # the document is public
```

Group, role, and parent-folder inheritance all work — OpenFGA resolves them
server-side; Mixpeek only relays the decision.

### Enforcement modes

Different strategies trade consistency against scale. Pick one with `mode`:

| `mode`              | Strategy                                                                                                               | Use when                                                 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `auto` *(default)*  | `ListObjects` → pre-filter when the accessible set is small (≤ `list_objects_max`), otherwise `BatchCheck` post-filter | You want sensible behavior at any size                   |
| `pull_list_objects` | Always `ListObjects` → `document_id` pre-filter                                                                        | The accessible set per user is small (≤ \~1000)          |
| `pull_batch_check`  | Run the search, then `BatchCheck` the candidates and drop unauthorized                                                 | Users can access many documents                          |
| `push`              | Sync OpenFGA grants into an indexed field and filter in-index                                                          | Lowest query latency; eventual consistency is acceptable |

<Note>
  Pre-filtering via `ListObjects` only scales to a small accessible set
  (\~1000 objects). For larger corpora, `BatchCheck` post-filtering **over-fetches
  by `over_fetch_factor` (default 2×)** from the vector store so the page stays
  full after unauthorized documents are dropped. `auto` switches between the two
  for you.
</Note>

### Configuration

Opt a namespace in by setting `infrastructure.authorization`:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH https://api.mixpeek.com/v1/namespaces/ns_abc123 \
    -H "Authorization: Bearer mxp_sk_your-org-key" \
    -H "Content-Type: application/json" \
    -d '{
      "infrastructure": {
        "authorization": {
          "enabled": true,
          "provider": "openfga",
          "api_url": "https://openfga.your-company.com",
          "store_id": "01J0XEXAMPLESTORE",
          "relation": "viewer",
          "mode": "auto"
        }
      }
    }'
  ```
</CodeGroup>

<ParamField path="enabled" type="boolean" required>
  Master switch. `false` (default) means the namespace is unchanged — no
  enforcement and no calls to OpenFGA.
</ParamField>

<ParamField path="provider" type="string" default="openfga">
  Authorization backend. `openfga` is the supported provider.
</ParamField>

<ParamField path="api_url" type="string" required>
  Base URL of your OpenFGA HTTP API, reachable from Mixpeek.
</ParamField>

<ParamField path="store_id" type="string" required>
  The OpenFGA store id holding your relationship tuples.
</ParamField>

<ParamField path="model_id" type="string">
  Authorization-model id. When omitted, the store's latest model is used.
</ParamField>

<ParamField path="api_token_secret_ref" type="string">
  Name of an [organization secret](/docs/operations/security) holding the bearer
  token for your OpenFGA API. The token is resolved from the encrypted secrets
  vault at request time and is never stored in your namespace config. This is
  the required way to supply a token for any hosted (`https://`) OpenFGA
  deployment — store the token once via the organization secrets API, then
  reference it here by name.
</ParamField>

<ParamField path="api_token" type="string" deprecated>
  Deprecated plaintext bearer token, accepted only for local development
  (`http://` or `localhost`) where OpenFGA runs without auth. Rejected for a
  hosted OpenFGA deployment — use `api_token_secret_ref` instead so the token
  is never sent or stored in plaintext.
</ParamField>

<ParamField path="object_type" type="string" default="document">
  The OpenFGA object type representing a Mixpeek document.
</ParamField>

<ParamField path="relation" type="string" default="viewer">
  The relation that grants read/retrieve access.
</ParamField>

<ParamField path="mode" type="string" default="auto">
  Enforcement strategy: `auto`, `pull_list_objects`, `pull_batch_check`, or `push`.
</ParamField>

<ParamField path="over_fetch_factor" type="integer" default="2">
  Post-filter over-fetch multiplier (≥ 2 recommended) so a page stays full after
  unauthorized documents are dropped.
</ParamField>

### Setup

<Steps>
  <Step title="Run OpenFGA and write your tuples">
    Stand up OpenFGA (or use your existing deployment) and write `viewer` tuples
    whose object ids are Mixpeek `document_id`s, e.g.
    `document:doc_abc123 # viewer @ user:alice`. Use `user:*` for public documents.
  </Step>

  <Step title="Create user-scoped keys per end-user">
    Generate a `usr_sk_` key whose `principal_id` matches the OpenFGA user id
    (e.g. `alice`). Create it with your org-scoped key via
    [`POST /v1/organizations/users/{user_email}/api-keys`](/docs/api-reference/organization-api-keys/create-api-key).
    There is no `key_type` field — setting `principal_id` on the request is what
    makes the key user-scoped (the response key carries the `usr_sk_` prefix).
  </Step>

  <Step title="Opt the namespace in">
    PATCH the namespace with the `infrastructure.authorization` block above.
  </Step>

  <Step title="Query normally">
    Execute retrievers with each user's `usr_sk_` key — results are filtered to
    what that user can see in OpenFGA. No query changes needed.
  </Step>
</Steps>

### Keeping permissions current (push mode)

In `push` mode, Mixpeek subscribes to your OpenFGA changelog and projects grants
onto an indexed field on each document, then filters in-index for the lowest
query latency. Grant and revocation changes propagate within the sync interval
(eventually consistent). For strict point-in-time consistency, use `auto` or
`pull_batch_check`, which evaluate against OpenFGA live on every query.

## Behavior and guarantees

* **Fail-closed.** If OpenFGA is unreachable, queries return the safe subset
  (fewer results), never unauthorized documents.
* **No caching leaks.** When authorization is active, the per-retriever result
  cache is bypassed so one user's authorized page can never be served to another.
* **Both retrieval paths.** Ad-hoc and saved retrievers enforce identically.
* **Opt-out is unchanged.** A namespace with `enabled: false` (or no
  `authorization` block) behaves exactly as before.

<Warning>
  Permissions are enforced at the Mixpeek API layer. If you have direct access to
  the underlying vector store or database, filters are not applied. Always route
  end-user traffic through the Mixpeek API.
</Warning>

## Related

* [Document Access Control](/docs/platform/operations#document-access-control) — Mixpeek's built-in `_acl` permissions
* [Operate](/docs/platform/operations) — authentication, secrets, and namespace isolation
* [Filters](/docs/retrieval/filters) — manual query filters (permission filters are injected on top)
* [OpenFGA documentation](https://openfga.dev/docs) — modeling, tuples, and the Check / ListObjects / BatchCheck APIs
