Huck separates the information you store from the identity allowed to retrieve it. Your server supplies both. Your visitor, the assistant and a retrieved document cannot grant access.
Start with the access shape you actually need
Report an issue with this sectionIn Access, define the keys and their parent relationships. Each section expands into examples generated from that exact path. Names help you recognize a section; the gate key is the API contract.
For an organization with teams and people, use org_id → team_id → user_id. An independent permission_id can remain a separate root. A record restricted to a person needs that person's organization, team and user identifier. A record restricted only to an organization needs just the organization identifier. A company-wide record uses an explicit empty gates object if the policy permits it.
Gate types are id, string, integer and uuid. id accepts integers or strings and compares their canonical string values; string "004" stays distinct from integer 4. integer requires a JSON integer, and UUID comparison normalizes case. Unknown gate keys are rejected. There are at most 32 keys and eight levels. Arbitrary fields inside record content do not need registration.
Flat keys remain independent until you publish their parent relationship. Do not assume that similarly named keys imply a relationship. Parent links describe required context, not actual membership: your application remains the authority for which organization and team a person belongs to.
Create and publish a hierarchy
Report an issue with this sectionUse access:read to read GET /api/v1/access/policy. The reply contains policy and version. Draft creation requires access:write; publication requires access:publish.
{
"expected_version": 1,
"nodes": [
{"key":"org_id","name":"Organization","type":"id"},
{"key":"team_id","name":"Team","type":"id","parent":"org_id"},
{"key":"user_id","name":"Person","type":"id","parent":"team_id"},
{"key":"permission_id","name":"Permission","type":"uuid"}
],
"policy": {
"allow_company_wide": true,
"default_gates": {},
"roles": {
"manager":{"scope":"org_id","bypass":["team_id","user_id"]},
"team_manager":{"scope":"team_id","bypass":["user_id"]},
"admin":{"read_all":true}
}
}
}
Send this object to POST /api/v1/access/drafts, replacing the sample version with the version you just read. The response contains the draft and saved tree. Publish with POST /api/v1/access/drafts/{draftId}/publish and {"expected_version":1}. Never assume a draft ID or reuse an obsolete version.
The server compares the actual saved tree with its draft policy, then validates active identities, active sources and enabled actions. An incompatible existing record stops publication; repair its complete context first. Changing the policy invalidates existing chat sessions and old evaluation evidence. A draft affects the privileged simulator only; ordinary chat uses the published policy.
Put information at exactly one level
Report an issue with this sectionCreate organization-wide information using the organization's gate:
{
"external_type":"handbook",
"external_id":"juniper-delivery",
"revision":1,
"content":{"delivery_days":["Tuesday","Thursday"]},
"gates":{"org_id":"2"}
}
An order visible to one person includes the complete path:
{
"external_type":"order",
"external_id":"1042",
"revision":1,
"content":{
"delivery":{"day":"Thursday","window":"afternoon"},
"items":[{"product":"T-shirt","variant":{"size":"M","color":"navy"},"quantity":2}]
},
"gates":{"org_id":"2","team_id":"support","user_id":"4"}
}
Both use POST /api/v1/knowledge, a server key with knowledge:write, and a stable Idempotency-Key for each intended change. The entire content object shares the record's audience. Store restricted fields as a separate record if their audience differs. Related-record links neither grant access nor automatically retrieve the linked content.
All supplied gates must match. An array within a leaf gate means any of those values. With a descendant present, each ancestor must contain exactly one value: org_id:["2","9"], user_id:"4" is rejected rather than creating an ambiguous cross-product. See knowledge ingestion for updates, batches, storage limits, tombstones and indexing readiness.
Register the person on your server
Report an issue with this sectionCreate a trusted subject using POST /api/v1/subjects, subjects:write and If-None-Match: *:
{
"external_id":"org:2:user:4",
"attributes":{"org_id":"2","team_id":"support","user_id":"4","role":"member"},
"active":true
}
The subject external ID is workspace-wide. Namespace it when your application reuses a user identifier in different organizations. Do not use browser-supplied organization or role values. Read membership from your own authenticated server records.
To change an existing subject, first call GET /api/v1/subjects/{externalId} with subjects:read, retain the returned ETag, and supply that exact tag in If-Match on the next POST. A missing precondition returns 428 for a changed existing identity; a stale tag returns 412. Resolve the conflict from your current membership data, not by automatically overwriting the latest version. Deactivating an identity also requires complete valid attributes.
If you supply only user_id:4 under this hierarchy, Huck returns 422 with the missing ancestor in errors, such as attributes.team_id. It does not infer membership, look up a parent from a label, or silently treat the identity as company-wide. A complete persisted subject can be referenced by external ID when minting chat; you do not repeat its gates in the session request.
Model managers and administrators deliberately
Report an issue with this sectionAn organization manager can have {"role":"manager","org_id":["2","9"]} without asserting a child identity. Their rule bypasses team/person gates only on records whose organization matches. Independent permission gates still apply.
A team manager includes their organization and team. Matching team_id:"support" in another organization is insufficient, even if both organizations reuse that team ID. A delegated rule cannot bypass an ancestor of its scope.
An explicit read_all role grants access across the workspace. {"role":"admin"} is valid when that role is published. However, adding a malformed or incomplete child identity to an administrator does not make it valid: the complete identity is checked before read-all grants. Do not send a person's ID with an incomplete path merely to personalize an administrator's context.
Company-wide content still belongs to the workspace. It never means public to other Huck customers. A visitor without a trusted session cannot query your workspace's knowledge.
Name organizations, people and permissions
Report an issue with this sectionNames explain authorized relationships without replacing gate IDs. Read GET /api/v1/access/labels with access:read, then use its current policy_version for PUT /api/v1/access/labels with access:write:
{
"key":"user_id",
"value":"4",
"context":{"org_id":"2","team_id":"support"},
"label":"Alice",
"expected_policy_version":2
}
Each child name requires exactly its complete, singular ancestor context. A root organization label has context:{}. Alice in one organization and Bob in another can both have user ID 4 without sharing a name. Old child names without provable parent context remain saved for owner repair but are not injected into chat. The Access screen shows entries that need their parent context updated.
Updates and deletion require the current version. Naming changes advance the policy version because authorized chat context and evaluation evidence changed. A label never fills in a missing parent, creates membership or grants access.
Test allowed and denied cases
Report an issue with this sectionUse the Access simulator to select a saved identity and inspect decisions without returning source content. Via API, send POST /api/v1/access/simulate with access:read:
{"subject_external_id":"org:2:user:4","draft_id":12}
Omit draft_id to simulate published access. Use the actual draft ID from creation. This is a privileged management endpoint, never an endpoint for an untrusted visitor. It shows at most 100 records. Incomplete saved identities fail before any results are shown.
Test at least: own record allowed, another user's record denied, same user ID in another organization denied, missing parent rejected, unrelated permission required, manager restricted to its scope, administrator valid only with a well-formed identity, inactive identity denied, and old session invalid after a policy change. Add goldens for representative answers and forbidden facts; evaluations describe regression checks against your real authorized context.
Mint the session and embed the assistant
Report an issue with this sectionYour authenticated server calls POST /api/v1/sessions with sessions:create:
{"subject":"org:2:user:4","origin":"https://app.example.com"}
Configure that exact origin in Settings. Return the full response to your widget's sessionProvider: token, expiration information, session_id and opaque context_id. Treat the Sanctum token as opaque and keep it in memory; do not decode it or put it in URLs. The server key never reaches browser JavaScript.
The session lasts at most 15 minutes and is bound to the subject, its version, the policy and the exact origin. The widget can refresh it through your authenticated server. context_id lets the widget refuse an automatic mutation retry after the identity or authorization context changes. On sign-out, revoke the chat session with chat.logout() and end your application session even if network cleanup fails. On account switch, destroy the old widget and discard conversation IDs before creating a new one.
See language integrations for Laravel authentication/CSRF, server clients and widget setup, and API keys and errors for the exact credential and retry contract.
Repair a context error safely
Report an issue with this sectionFor 422, inspect the named field and your published tree. Add the missing ancestor from trusted membership data; do not copy a value from another user's response. For 401, renew the chat session or replace an expired/revoked server key. For 403, inspect key scope, ownership or the explicitly delegated role. For 409/412/428, reload and reconcile before retrying.
During a hierarchy upgrade, Huck retains existing source records and subjects without inventing missing parents. Incomplete historical records fail closed until repaired. Correct the identity using its ETag and update source gates with a higher revision. A source's indexing error invalid_access_context requires correcting its audience before retrying indexing. A malformed saved tree or incompatible default audience must be repaired before migration/publication can succeed.
Use the report link beside the relevant section if these instructions do not match what the application does. Include the request ID and a redacted example; omit service keys, session tokens and private customer content.