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

# Access Control

> Restrict which people can retrieve a document, by declaring permissions yourself or letting connectors copy them from the source app.

By default, a query can retrieve any document in its collection. Access control changes that: a document carries an `acl`, the list of people allowed to retrieve it, and a query carries an `acl` naming who is asking. HydraDB returns only matching documents that person is allowed to see. This is how an internal search product keeps one employee's query from surfacing a private Slack channel or a restricted Drive file that belongs to someone else.

ACLs apply to knowledge sources. You set one at ingest on an [app source](/essentials/v2/app-sources), or later on an existing source ([section 4](#4-change-or-revoke-access)). [Connectors](/essentials/v2/connectors) can capture app permissions or apply your resource rules. [Memories](/essentials/v2/memories) are scoped per user with `collection` instead.

In this guide:

1. [Set an ACL on an item and query as someone](#1-set-an-acl-on-an-item-and-query-as-someone): restrict a Slack message, then query as one person.
2. [Principals](#2-principals): the five identity forms, and what a query's `acl` list can contain.
3. [Let connectors capture permissions](#3-let-connectors-capture-permissions): which providers copy their own permissions, and how your own rule per resource fits in.
4. [Change or revoke access](#4-change-or-revoke-access): editing an ACL without re-ingesting, and when each change takes effect.
5. [Common mistakes](#common-mistakes) and [related pages](#related).

## 1. Set an ACL on an item and query as someone

This example ingests one Slack message that only Grace and the members of channel `C0123` may retrieve, then runs a query on Grace's behalf. Every app source takes the same `acl` list; only the item changes.

<Accordion title="Ingest a restricted message and query as Grace: Python, TypeScript, or cURL">
  Set up your [SDK client](/api-reference/v2/sdks#client-setup) before running the Python or TypeScript example.

  <CodeGroup>
    ```python Python SDK theme={"dark"}
    import json
    import os
    from hydra_db import HydraDB

    client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])

    message = {
        "id": "slack-C0123-1712345678",
        "database": "acme_corp",
        "collection": "engineering",
        "title": "Q3 comp bands",
        "kind": "message",
        "provider": "slack",
        "external_id": "1712345678.000100",
        "fields": {"kind": "message", "body": "Q3 comp bands are attached."},
        "acl": ["user_email:grace@acme.com", "group:slack:C0123"],
    }

    # 1. Ingest the message with its allow-list.
    client.context.ingest(
        type="knowledge",
        database="acme_corp",
        collection="engineering",
        app_knowledge=json.dumps([message]),
    )
    ```

    ```typescript TypeScript SDK theme={"dark"}
    import { HydraDBClient } from "@hydradb/sdk";

    const client = new HydraDBClient({
      token: process.env.HYDRA_DB_API_KEY,
    });

    const message = {
      id: "slack-C0123-1712345678",
      database: "acme_corp",
      collection: "engineering",
      title: "Q3 comp bands",
      kind: "message",
      provider: "slack",
      external_id: "1712345678.000100",
      fields: { kind: "message", body: "Q3 comp bands are attached." },
      acl: ["user_email:grace@acme.com", "group:slack:C0123"],
    };

    // 1. Ingest the message with its allow-list.
    await client.context.ingest({
      type: "knowledge",
      database: "acme_corp",
      collection: "engineering",
      appKnowledge: JSON.stringify([message]),
    });
    ```

    ```bash cURL theme={"dark"}
    # 1. Ingest the message with its allow-list.
    curl -X POST 'https://api.hydradb.com/context/ingest' \
      -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
      -H "API-Version: 2" \
      -F "type=knowledge" \
      -F "database=acme_corp" \
      -F "collection=engineering" \
      -F 'app_knowledge=[
        {
          "id": "slack-C0123-1712345678",
          "database": "acme_corp",
          "collection": "engineering",
          "title": "Q3 comp bands",
          "kind": "message",
          "provider": "slack",
          "external_id": "1712345678.000100",
          "fields": { "kind": "message", "body": "Q3 comp bands are attached." },
          "acl": ["user_email:grace@acme.com", "group:slack:C0123"]
        }
      ]'
    ```
  </CodeGroup>

  Wait for [`GET /context/status`](/api-reference/v2/endpoint/source-status) to report `completed` for `slack-C0123-1712345678` in the `engineering` collection, then query as Grace.

  <CodeGroup>
    ```python Python SDK theme={"dark"}
    # 2. Query as Grace. Only documents she may see come back.
    result = client.query(
        database="acme_corp",
        collection="engineering",
        query="what are the Q3 comp bands",
        acl=["grace@acme.com"],
    )
    ```

    ```typescript TypeScript SDK theme={"dark"}
    // 2. Query as Grace. Only documents she may see come back.
    const result = await client.query({
      database: "acme_corp",
      collection: "engineering",
      query: "what are the Q3 comp bands",
      acl: ["grace@acme.com"],
    });
    ```

    ```bash cURL theme={"dark"}
    # 2. Query as Grace. Only documents she may see come back.
    curl -X POST 'https://api.hydradb.com/query' \
      -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
      -H "API-Version: 2" \
      -H "Content-Type: application/json" \
      -d '{
        "database": "acme_corp",
        "collection": "engineering",
        "query": "what are the Q3 comp bands",
        "acl": ["grace@acme.com"]
      }'
    ```
  </CodeGroup>
</Accordion>

### What each property means

| Property | What it is |
| - | - |
| `acl` on the item | Who may retrieve this document, as a list of [principals](#2-principals). Omit it and the document is unrestricted. A malformed entry rejects the whole request with `400` rather than ingesting the document unprotected. |
| `acl` on the query | Who is asking. Usually the caller's email; HydraDB adds the matching `domain:` principal and `__public__` for you. Send `group:` principals explicitly when the caller belongs to a group. |
| `id`, `kind`, `provider`, `external_id`, `fields` | The app source itself. [App Sources](/essentials/v2/app-sources) explains them. |

### What a query returns

A document comes back when its `acl` is absent (unrestricted), when it is `__public__`, or when it names one of the caller's principals. HydraDB checks this on every search path (semantic, keyword, and graph), so a document the caller may not see never reaches the results. Take six documents in one collection:

| Document | Its `acl` |
| - | - |
| Refund policy | none |
| Company handbook | `["__public__"]` |
| Grace's 1:1 notes | `["grace@acme.com"]` |
| `#incidents` thread | `["group:slack:C0123"]` |
| Q3 plan, shared with the whole company | `["domain:acme.com"]` |
| Bob's performance review | `["bob@acme.com"]` |

The same question returns different documents depending on who asks:

| Query `acl` | Returns |
| - | - |
| No `acl` | All six. The query is not filtered. |
| `["grace@acme.com"]` | Refund policy, company handbook, Grace's notes, and the Q3 plan, because Grace's email also matches `domain:acme.com`. |
| `["grace@acme.com", "group:slack:C0123"]` | The four above, plus the `#incidents` thread. |
| `["alex@partner.com"]` | Refund policy and company handbook only. |

Access control works alongside [metadata filters](/essentials/v2/metadata): filters say what you are looking for, ACLs say what you are allowed to find, and a filter can never widen what a caller may see.

`POST /context/list` takes the same `acl` field with the same meaning. The `GET /context/chunks`, `GET /context/relations`, and `GET /context/subgraph` endpoints take it as a query parameter, repeated (`acl=a&acl=b`) or comma-separated.

<Warning>
  Access control is opt-in per request. A query without an `acl` field is not filtered and returns everything in scope. Adding ACLs to your documents changes nothing until your queries declare who is asking.
</Warning>

## 2. Principals

A principal is an identity written as a string: one person, a group, or everyone in a domain. These are the five forms:

| Principal | Meaning |
| - | - |
| `user_email:grace@acme.com` | One person, by email. A bare `grace@acme.com` is accepted and stored in this form. |
| `domain:acme.com` | Everyone whose email is under that domain. A query as `grace@acme.com` matches it automatically; you do not declare it on the query side. |
| `group:<provider>:<id>` | A group in the source app, for example `group:slack:C0123` or `group:google:eng@acme.com`. Both parts are required. |
| `__public__` | Every identified caller in the collection. |
| `__private__` | Nobody. The stored form of an explicitly empty allow-list. |

Principals are lowercased, trimmed, and deduplicated on the way in. `__public__` overrides everything else in the same list: a public document is public. `__private__` only means something on its own; alongside real principals it is dropped. A document holds at most 1,000 principals of at most 256 characters each. For larger audiences use a `group:` or `domain:` principal.

**Absent is not empty:** a document with no `acl` is unrestricted. Every document ingested before you adopted access control behaves this way, so adopting it never hides existing documents. A document you restrict to nobody is stored as `__private__`; on an item, `"acl": []` means nobody, not everybody.

### What the query's `acl` list can contain

| You send | Behavior |
| - | - |
| No `acl` field | No filtering. |
| `"acl": []` | Same as omitting it. On a query, an empty list does not mean nobody. |
| `"acl": ["*"]` | Same as omitting it. An explicit "search as an administrator". |
| `"acl": ["grace@acme.com"]` | Filtered to what Grace may see: her own documents, anything shared with `domain:acme.com`, public documents, and unrestricted documents. |
| `"acl": ["grace@acme.com", "group:slack:C0123"]` | Grace's view plus everything shared with that group. Group membership is never derived from an email; declare it. |
| `"acl": ["__private__"]` | Public and unrestricted documents only. |

Several emails in one list see the union of their views. An entry that is neither an email nor a recognized principal is kept as-is and matches nothing but public and unrestricted documents. A typo narrows results; it never widens them. If a caller sees less than you expect, check the principal spelling first.

## 3. Let connectors capture permissions

For supported providers, HydraDB reads the source app's own permissions on every sync and applies them as ACLs, so you do not maintain a parallel permission model:

| Provider | What is captured |
| - | - |
| **Slack** | Public channels are visible workspace-wide; private channels only to their members, resolved to member emails. |
| **Google Drive** | Per-file sharing: user, group, domain, and public grants. Permission-only changes (a share with no edit to the file) are picked up through the Drive changes feed, which content sync alone cannot see. |
| **GitHub** | Repository visibility. Private repos additionally capture the collaborator list when every collaborator has a visible public email; otherwise your resource rule governs. |
| **Confluence** | Space-level view permissions, with groups expanded to member emails, plus per-page view restrictions. |
| **Jira** | Who holds Browse access per project, plus per-issue security levels. |

Capture is also built for GitLab, Linear, Dropbox, Zendesk, PagerDuty, Typeform, Attio, LaunchDarkly, ServiceNow, Microsoft Teams, and Outlook, and is switched on per provider. The provider catalog does not yet flag which providers capture permissions, so treat this list as the current set, and check [Get Connector Status](/api-reference/v2/endpoint/get-connector-status) for an `acl_warning` after the first sync. For any other provider, [set your own rule on the resource](#set-your-own-rule-on-a-resource).

### Set your own rule on a resource

Every object synced from a resource inherits the resource's rule. Set it when you [configure the connector](/api-reference/v2/endpoint/configure-connector), or change it later on its own with [Update Connector Resource](/api-reference/v2/endpoint/update-connector-resource). The change applies to every already-synced document from that resource on the next query, with no re-sync and no re-index.

<Accordion title="Set a resource rule at configure time or afterwards: JSON, Python, TypeScript, or cURL">
  At configure time, each entry in `resources` can carry its own `acl`:

  ```json theme={"dark"}
  {
    "resources": [
      {
        "resource_id": "C_LEADERSHIP",
        "resource_type": "channel",
        "name": "leadership",
        "acl": ["user_email:grace@acme.com", "user_email:hopper@acme.com"]
      }
    ]
  }
  ```

  Afterwards, change the rule on its own without touching anything else:

  <CodeGroup>
    ```python Python SDK theme={"dark"}
    client.connectors.update_resource_acl(
        id="conn_123",
        resource_id="C_LEADERSHIP",
        request={"acl": ["user_email:grace@acme.com"]},
    )
    ```

    ```typescript TypeScript SDK theme={"dark"}
    await client.connectors.updateResourceAcl({
      id: "conn_123",
      resourceId: "C_LEADERSHIP",
      body: { acl: ["user_email:grace@acme.com"] },
    });
    ```

    ```bash cURL theme={"dark"}
    curl -X PATCH 'https://api.hydradb.com/connectors/conn_123/resources/C_LEADERSHIP' \
      -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
      -H "API-Version: 2" \
      -H "Content-Type: application/json" \
      -d '{ "acl": ["user_email:grace@acme.com"] }'
    ```
  </CodeGroup>
</Accordion>

### When the provider's permissions and your rule disagree

* **A per-document permission from the provider wins over everything:** a Drive file's own sharing decides that file.
* **A provider verdict of "this resource is public" does not override a rule you set:** restricting a public channel is a deliberate act, and enabling capture never widens it back.
* **Otherwise the provider's resource-level verdict wins over your rule:** the provider is the fresher source of truth.

If HydraDB cannot read a resource's permissions from the provider, a rule you set still applies. A resource with no rule of yours is readable by everyone until the next successful read. [Get Connector Status](/api-reference/v2/endpoint/get-connector-status) reports an `acl_warning` on the resource meanwhile, and a `page_acl_warning` when restrictions on some individual pages, for example in Confluence, could not be read.

## 4. Change or revoke access

To change who may retrieve a document you already ingested, send `acl` to [`PATCH /context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata). Unlike the metadata fields on that endpoint, `acl` replaces the stored list rather than merging into it: to add a third person, send all three. An `acl`-only body is a valid edit.

<Accordion title="Replace a document's ACL without re-ingesting: Python, TypeScript, or cURL">
  <CodeGroup>
    ```python Python SDK theme={"dark"}
    client.context.update_source_metadata(
        "slack-C0123-1712345678",
        database="acme_corp",
        collection="engineering",
        acl=["user_email:grace@acme.com", "user_email:hopper@acme.com"],
    )
    ```

    ```typescript TypeScript SDK theme={"dark"}
    await client.context.updateSourceMetadata({
      id: "slack-C0123-1712345678",
      database: "acme_corp",
      collection: "engineering",
      acl: ["user_email:grace@acme.com", "user_email:hopper@acme.com"],
    });
    ```

    ```bash cURL theme={"dark"}
    curl -X PATCH 'https://api.hydradb.com/context/slack-C0123-1712345678/metadata' \
      -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
      -H "API-Version: 2" \
      -H "Content-Type: application/json" \
      -d '{
        "database": "acme_corp",
        "collection": "engineering",
        "acl": ["user_email:grace@acme.com", "user_email:hopper@acme.com"]
      }'
    ```
  </CodeGroup>
</Accordion>

The three wire states are distinct:

| You send | Result |
| - | - |
| No `acl` key | The stored ACL is left untouched. |
| `"acl": [...]` | Replaced with exactly that list. |
| `"acl": []` or `"acl": null` | Revoked. Nobody can retrieve it. |

The response carries `acl_updated: true`. If `vector_acl_synced` is missing from it, the new list is still enforced, but a newly added person may miss this document in results until it is re-indexed.

### When a change takes effect

| Change | When it takes effect |
| - | - |
| `PATCH /context/{id}/metadata` with a new `acl` | Immediately, on the next query. |
| `PATCH /connectors/{id}/resources/{resource_id}` with a new `acl` | Immediately, on the next query, for every document from that resource. |
| A permission change in the source app (a channel goes private, a Drive share is removed) | On the connector's next sync cycle. |

Deleting a rule does not widen access on its own: visibility only ever widens from a value you positively set. To open a restricted resource back up, set its ACL to `["__public__"]` rather than clearing it.

## Common mistakes

<AccordionGroup>
  <Accordion title="ACLs are set but every query still returns everything">
    The queries are not declaring an identity. Add `"acl": ["<caller email>"]` to each query.
  </Accordion>

  <Accordion title="A caller who should see a document sees nothing">
    Check the principal forms on both sides. `group:slack:C0123` on the document only matches a query that declares that same group; unlike `domain:`, group membership is not derived from the caller's email. Also confirm the email matches exactly: principals are compared after lowercasing and trimming, but not otherwise fuzzy-matched.
  </Accordion>
</AccordionGroup>

## Related

* [Connectors](/essentials/v2/connectors): syncing app data, and per-resource ACL rules
* [App Sources](/essentials/v2/app-sources): the ingest shape that carries `acl`
* [Query](/essentials/v2/query): the `acl` field alongside every other retrieval parameter
* [Metadata](/essentials/v2/metadata): filtering by attributes, a different question from permission
* [Multi-tenancy](/essentials/v2/multi-tenant): databases and collections, the isolation boundary ACLs work inside
* [Update Source Metadata (API Reference)](/api-reference/v2/endpoint/update-source-metadata): the `acl` replacement contract
* [Update Connector Resource (API Reference)](/api-reference/v2/endpoint/update-connector-resource): the per-resource rule


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