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 access-control list (ACL): the identities 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. 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 an identity represented by a string: a user, a group, or everyone in a domain. These are the five supported 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. For larger groups, use a group: or domain: principal.

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. Capture is also built for other providers, including GitLab, Linear, Dropbox, Zendesk and PagerDuty, and is switched on per provider. For any other provider, set a rule per resource with acl, as described above. If HydraDB cannot read a resource’s permissions from the provider, a resource with no rule of yours is readable by everyone until the next successful read, while a rule you set still applies. Get Connector Status reports an acl_warning on the resource meanwhile. 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 can be turned off per provider without a deploy.

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 (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 grace@acme.com automatically matches anything shared with domain:acme.com.
Send group principals explicitly when you want them: "acl": ["grace@acme.com", "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.
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.