Skip to main content
By default every document in a collection is retrievable by every query against it. Access control changes that: a document can carry an ACL - a list of principals allowed to retrieve it - and a query can carry the identity it is running on behalf of. HydraDB returns only the documents that identity is allowed to see. This is what you need to build an internal search product where a query by one employee must not surface a private Slack channel or a restricted Drive file belonging to another.
Access control is opt-in per request. A query without an acl field is not filtered, exactly as before. Adding ACLs to your documents changes nothing until your queries start declaring who is asking.

1. The two halves

Access control only works when both halves are in place. Either half alone is a no-op.

2. Principals

A principal is one string identifying who may retrieve a document. Five forms: Principals are lowercased, trimmed, and deduplicated on the way in. __public__ overrides everything else in the same list - a document that is public is public. A list containing __private__ alongside real principals keeps the real principals and drops the sentinel; __private__ only means something on its own.
An absent ACL and an empty ACL are not the same thing. A document with no acl is unrestricted - that is how every document ingested before you adopted access control behaves, and it is why adopting it never silently hides your existing content. A document you explicitly restrict to nobody is stored as __private__. Sending "acl": [] means “nobody”, not “everybody”.
Limits: 1000 principals per document, 256 characters per principal. Past that, use a group: or domain: principal - a list of several thousand individuals is organizational structure, not an allow-list.

3. Set an ACL

At ingest, on an app source

Each item in app_knowledge accepts an acl list:
Omit acl and the document is unrestricted. A malformed principal rejects the whole request with 400 rather than ingesting the document unprotected.

On an existing source, without re-ingesting

PATCH /context/{id}/metadata accepts acl and replaces the stored list:
Replace, not merge: to add a third person, send all three. The three wire states are distinct and all meaningful: An acl-only body is a valid edit; you do not have to send metadata alongside it.

On a connector resource

Every object synced from a resource inherits the resource’s rule. Set it when you configure the connector:
Or change it later, on its own, without touching anything else:
This takes effect on every already-synced document from that resource, on the next query. There is no re-sync and no re-index, however many documents the resource has. Use ["__public__"] to open a resource back up.

4. Let connectors capture permissions for you

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. Check what is live for your account with GET /connector-catalog: each provider carries rbac_support and a one-line rbac_description of what it captures. Precedence, when both exist:
  • A per-document permission from the provider (a Drive file’s own sharing) wins over everything.
  • A provider verdict of “this resource is public” will 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, because the provider is the fresher source of truth.
Capture is per provider and can be turned off without a deploy. A provider without capture is not broken - your own rules still work on it, and documents remain unrestricted until you set one.

5. Query on behalf of someone

Pass the caller’s identity as acl on POST /query (and POST /context/list, which takes the same field with the same meaning):
A document is returned when any of these holds:
  • its stored ACL is absent or empty (unrestricted),
  • it is __public__,
  • its ACL contains one of the caller’s principals.
Two conveniences mean you rarely send more than an email:
  • __public__ is added for you. You never have to ask for public content.
  • The domain principal is derived from the email. Querying as [email protected] automatically matches anything shared with domain:acme.com.
Send group principals explicitly when you want them: "acl": ["[email protected]", "group:slack:C0123"].
An entry that is neither an email nor a recognized principal is kept as-is and matches nothing but public content. A typo narrows results; it never widens them. If a caller sees less than you expect, check the principal spelling first.
Access control composes with, and is independent of, metadata filters: filters express what you are looking for, ACLs express what you are allowed to find. A caller cannot widen their own visibility with a filter.

6. Revoking access

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.

7. Common mistakes

The queries are not declaring an identity. acl is opt-in per request; without it there is no filtering. Add "acl": ["<caller email>"] to the query.
"acl": [] means nobody, and is stored as __private__. To leave a document unrestricted, omit the field entirely. To make it visible to everyone, send ["__public__"].
The acl on PATCH /context/{id}/metadata replaces the list, it does not merge. Send the complete new allow-list every time.
A provider-side permission change is picked up on the next sync cycle for that resource. A rule you set yourself through the API applies to already-synced documents on the next query, with no sync involved.
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.