The handbook
TenantsUsers and rolesApplicationsScopesAgentsContext (graphium)CultivatorsChatsTracingAudit log
Concepts

Concepts: Applications

An application is the caller — the api-key-holding, non-human identity that actually makes requests to AcasoOS. A WhatsApp connector's backend, an internal service, a partner integration, the management console itself: each is an application. It is distinct from a user, which is the human an application acts as. The platform keeps this split everywhere: the querier (the api-key holder) and the subject (the person a request is about) are never the same record. The console exposes applications as the Applications page.

What an application is

An application record (dh_applications) is the durable identity for a caller within a tenant:

FieldPurpose
idUUID, immutable.
slugStable, URL-safe handle, unique per tenant. Immutable.
display_nameHuman-friendly name shown in the console and the audit log.
scope_ceilingThe coarse cap on everything this application may do or broker.
statusactive, suspended, or revoked.
metadataFree-form JSONB for integration-specific data.

An application holds no credentials itself — it owns one or more API keys, and those keys are what authenticate a request.

Keys

Every API key (dh_api_keys) belongs to exactly one application (application_id). A request authenticates by presenting a key as Authorization: Bearer <key>; AcasoOS resolves the key to its owning application and that application's ceiling.

  • Long-lived keys are minted for an application and stored by the calling backend. make mint-key mints one owned by the built-in Management Console application.
  • Session keys (kind=session) are short-lived tokens minted from a long-lived parent via POST /v1/auth/session. They inherit the parent's application and expire on their own clock; revoking the parent kills every session derived from it. The console authenticates this way — see Security model.

Minting and revoking keys is covered in Workflows: Mint and revoke API keys.

Acting as a person

By default an application acts as itself: a request with no person header runs under the application's own ceiling. To run a request on behalf of a human, the backend adds a header naming the person:

Authorization: Bearer <application-key>
X-Acaso-Person-Id: <user-uuid>

(The legacy X-Acaso-User-Id header is still accepted.) When a person is named, the auth flow checks three things before the request proceeds:

CheckIf it fails
The owning application is active.403 ERR.AUTH.0012 — a suspended application disables all its keys.
The application's ceiling grants users:impersonate.403 ERR.AUTH.0008 — the application is not trusted to broker a person.
The named person exists in this tenant, is active, and is a person (not a system identity).403 ERR.AUTH.0006.

When the checks pass, the request runs as that person, with scopes bounded by the application's ceiling.

Scope ceiling and bounded delegation

The scope_ceiling is the maximum authority an application can ever exercise or pass through. When it acts as a person, the effective scopes are the intersection of the person's scopes with the ceiling, plus the person's own un-capped self-read:

effective_scopes  =  (person_scopes  ∩  scope_ceiling)  +  self:<person_id>:read

So an application can never grant a person more than the person already holds, and a person can never gain more through an application than that application is trusted to broker — whichever is narrower wins. The intersection is wildcard-aware in both directions: a person scope broader than a ceiling literal (context:* over context:read) is narrowed to the ceiling rather than dropped.

A ceiling is validated when it is set: bare * is rejected, only an admin may grant acaso_os:admin, and a non-admin may only grant ceiling scopes they themselves hold — otherwise applications:write would be a confused-deputy escalation. See Concepts: Scopes for the matching algorithm.

Status and lifecycle

StatusEffect
activeNormal operation.
suspendedEvery key the application owns stops authenticating immediately (403 ERR.AUTH.0012). Reversible.
revokedTerminal. Keys are filtered out at resolution time.

Changing status, tightening a ceiling, or revoking a key invalidates the affected keys' cache entries within seconds — a Redis pubsub event drops them on every replica — so a tightened ceiling takes effect almost at once rather than waiting out the cache TTL.

A worked example

A WhatsApp connector links a tenant's employees to an agent:

  1. An admin registers an application whatsapp-connector with a ceiling of ["agents:*:run", "context:read", "users:impersonate"] and mints a key for it.
  2. The connector's backend stores that one key. WhatsApp never sees AcasoOS, and AcasoOS never holds a per-person key.
  3. An employee messages the WhatsApp number. The connector maps that phone number to a dh_users record and calls AcasoOS with Authorization: Bearer <whatsapp-connector key> and X-Acaso-Person-Id: <employee uuid>.
  4. Auth resolves the key to whatsapp-connector, confirms the ceiling permits users:impersonate, loads the employee, and computes effective scopes as (employee's role scopes ∩ ceiling) + self-read.
  5. The agent answers as that employee — able to run agents and read context, but nothing the ceiling forbids, even if the employee's own roles would otherwise permit more.

One application, one key, many people: each request declares who it is for, and the platform enforces the bound.

PreviousConceptsUsers and rolesNext ConceptsScopes