Authentication: two identities
Authorization needs aPrincipal to reason about; authentication is how a request acquires one.
HyphaeDB has two authentication paths, one per plane:
- Data plane (
/v1) — an agent presents an API key (x-hyphae-key: hyk_...) on every gRPC / REST / WebSocket call. The key resolves to a registeredPrincipalcarrying the agent’s roles and tenant. That principal — not any client-supplied field — is the source of the agent’s identity on every write (see write-scope binding). - Control plane (
/admin/api) — an operator logs in through an OIDC Authorization-Code flow that establishes a server-side session (an opaqueHttpOnlycookie). The federated subject resolves to a localPrincipalwith an admin role. See the Admin control plane for the login sequence and session handling.
CredentialProvider verifies the presented
credential — an API-key hash, or an OIDC ID token checked against the issuer’s JWKS — and yields the
authenticated Principal. New credential types plug in behind that trait without touching the authz
model below.
The registered-principal gate. Whether an unregistered request is admitted at all is a
deployment-profile control. In
production, require_registered_principal is enforced — every request
must resolve to a registered principal or it is rejected. In dev, an unregistered caller may be minted
a fixed, empty-roles dev-unregistered principal, which under deny-by-default can then do nothing until
a role is granted. See Deployment.The authorizer
The decision point is a single, stateless function:Forbidden. Otherwise it returns the narrowest AccessScope the matching bindings grant — a tenant,
a set of allowed projects, and an agent visibility.
Scoped reads
The application-facing reads take anAccessScope and compile it into a mandatory SQL WHERE
predicate served by indexes:
nearest_neighbors_scoped— returns the next in-scope neighbors, not global top-k minus the out-of-scope ones.get_node_scoped— returns a node only if it is in scope; otherwiseNone, indistinguishable from “does not exist” so callers cannot probe for out-of-scope IDs.get_inbox_scoped— an agent reads its own inbox unless its visibility permits cross-agent reads.list_scenes_scoped— scenes within the caller’s tenant and allowed projects.
Write-scope binding
On every write —store and gossip ingress — the persisted source_agent and tenant_id are taken
from the authenticated principal, never from client-supplied body fields. A body that claims a
different source_agent or tenant_id is rejected with Forbidden rather than silently corrected, so
the mismatch is auditable.
This is what makes provenance unforgeable: a client cannot author as another agent or write into
another tenant. See /concepts/trust-and-provenance for how
provenance is then used downstream.
Authorization is a hard allow/deny plus scope. Trust scoring is a separate, soft signal — it
down-weights, it does not deny. The two work together: authorization decides reachability, trust
decides ranking within what is reachable.
The tenant predicate
Every scoped read AND-stenant_id = scope.tenant_id unconditionally — even in V1, where every
principal is minted into a single default tenant, so the predicate is a no-op partition today. Because
the field and predicate exist from day one, turning on hard tenant isolation later is a configuration
and issuance change, not a schema or API redesign.