Reference

Credentials

A session identifies a person, an API key identifies an agent, and neither can do the other’s job. That separation is the security model.

On this page

Two kinds

IdentifiesAuthority
SessionA personBetter Auth owns users, sessions and organisations. The dashboard uses these, and it is the only thing that can create an organisation or mint a key.
API keyAn agentOpaque, scoped, individually revocable, stored as a sha256 hash. Resolves to one organisation and a set of scopes, and can only read and write that organisation’s memory.

A key looks like cmi_<env>_<id>_<secret>, where the environment half is dev or prod from COGNITIVE_MEMORY_ENV. The secret is shown exactly once, because only its hash is stored.

The dashboard is authenticated by session and /api/dashboard/* never accepts an API key. Reaching for a key to look at your own memory would train exactly the habit this service is trying to discourage.

The separation is the model

A leaked agent key cannot mint a new key, cannot change its own scopes, and cannot create an organisation. Rotation happens from a signed-in session.

That is not a limitation of the current implementation, it is the reason the two authenticators exist. Keys are how a caller escalates, so minting one must require the human.

There is one exception, and it is deliberate: a keys:manage scope lets a key rotate its siblings for the same organisation, which is the CI case. It still cannot create an organisation and it still cannot widen its own scopes.

Every read is filtered by organisation id in one auditable layer, so cross-tenant access is a single check rather than a property every query has to remember.

Why sha256

Keys are stored as a sha256 hash, not a slow key derivation like bcrypt or argon2. That looks like a mistake and is not.

Password hashing exists to make guessing a human password expensive. These secrets carry 256 bits of entropy, so there is no search to slow down, and a deliberately slow hash would add latency to every request to protect against an attack that cannot happen.

The public prefix means a presented key is one indexed lookup and one constant-time comparison — the prefix narrows the candidate row before the hash is computed at all.

Issuing, scoping, revoking

From the dashboard, or over HTTP from a signed-in session. A new key gets memories:read, memories:write and stats:read; narrow it to what the agent actually does.

sh
# Session-authenticated: a cookie, not a key.curl -X POST $COGNITIVE_MEMORY_URL/api/v1/keys \  -H "content-type: application/json" \  -b "$SESSION_COOKIE" \  -d '{"name":"ci","scopes":["memories:read","memories:write"],"expiresInDays":30}'

Keys carry an optional expiry and report a status of active, expired or revoked, alongside lastUsedAt — so an unused key is visible as unused rather than merely never mentioned.

Every key is individually revocable, and revocation is immediate. A present key that is revoked, expired or unknown answers 401 Unauthorized rather than 403 — the key is not wrong, it no longer exists. A valid key missing a scope gets 403 with requiredScope naming it. The error table has the rest.

Before production

BETTER_AUTH_SECRET is required in production, and GET /api/v1/health reports its absence in problems rather than failing a sign-in at runtime.

Every read is filtered by organisation, so the thing to get right before real data is the organisation boundary rather than the key format. Theconfiguration reference lists the two auth values and everything else.