Private fields
Keep fields such as cost price or supplier id searchable for trusted callers while they never reach the widget, the assistant or unprivileged keys.
Some fields belong in the index but not in a public response: cost_price,
supplier_id, margin, internal notes. Mark them private and they stay
available to trusted callers only.
Marking fields private
Index → Search settings → Private fields. The list is stored in
settings.search.privateFields (at most 64 fields). Saving checks that every field
exists in the schema and that the index's default query_by, facet_by,
sort_by and highlight settings do not use a private field — those defaults apply
to every unprivileged search, so the save is refused with PRIVATE_FIELD instead.
Search profiles are checked the same way when they are saved.
Who sees private fields
| Caller | Private fields |
|---|---|
API key with documents:read_private (or *) | Yes |
| API key without that scope | No |
Embed widget (/embed/*) | Never — the token is public by construction |
| Assistant, answer, RAG | Never — the model context must not see them |
GET /documents/export | Only with documents:read_private; otherwise every line is stripped |
Snapshot download (GET /indexes/{slug}/snapshots/{id}) | Only with documents:read_private; otherwise download is null |
Scoped keys (POST /indexes/{slug}/scoped-key) | Not issued at all for an index with private fields unless the issuing key has documents:read_private (400 PRIVATE_FIELDS_PRESENT): a browser holding the key could filter on a private field and read found |
| Console | Organization owners and admins yes, members no (documents opened by a member arrive without them) |
What is removed
For a caller without access, a private field disappears from every place a value
can surface: the document in each hit, highlights, text-match info, facet counts,
grouped hits, explain payloads, GET /documents/{id} and the export. Rerankers
never receive private fields for an unprivileged call.
Referencing a private field is an error
Stripping the value is not enough: a filter such as cost_price:<10 would still
reveal the value by what matches. A request that names a private field in
query_by, filter_by, sort_by, facet_by, include_fields or group_by
without the scope is rejected:
curl -sS "$AAC_API/indexes/products/search?q=drill&filter_by=cost_price:<10" \
-H "Authorization: Bearer $AAC_KEY"{
"error": {
"code": "PRIVATE_FIELD",
"message": "This request references private fields (cost_price); a key with the documents:read_private scope is required",
"details": { "fields": ["cost_price"], "params": ["filter_by"] },
"request_id": "req_…"
}
}The status is 400. POST /embed/search answers the same 400 when a search names a private field.
The check runs on what the engine would receive: facetFilters are merged into
filter_by first, and field names are compared after trimming quotes, spaces and
! — " cost_price:10" is refused like cost_price:10. The facet-value search
(/facets/{attribute}/search) and /assistant/native apply the same rule.
Granting the scope
Create a server-side key with documents:read_private in addition to
search:read. Never use such a key in a browser.