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:
| Field | Purpose |
|---|---|
id | UUID, immutable. |
slug | Stable, URL-safe handle, unique per tenant. Immutable. |
display_name | Human-friendly name shown in the console and the audit log. |
scope_ceiling | The coarse cap on everything this application may do or broker. |
status | active, suspended, or revoked. |
metadata | Free-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-keymints one owned by the built-in Management Console application. - Session keys (
kind=session) are short-lived tokens minted from a long-lived parent viaPOST /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:
| Check | If 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>:readSo 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
| Status | Effect |
|---|---|
active | Normal operation. |
suspended | Every key the application owns stops authenticating immediately (403 ERR.AUTH.0012). Reversible. |
revoked | Terminal. 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:
- An admin registers an application
whatsapp-connectorwith a ceiling of["agents:*:run", "context:read", "users:impersonate"]and mints a key for it. - The connector's backend stores that one key. WhatsApp never sees AcasoOS, and AcasoOS never holds a per-person key.
- An employee messages the WhatsApp number. The connector maps that phone number to a
dh_usersrecord and calls AcasoOS withAuthorization: Bearer <whatsapp-connector key>andX-Acaso-Person-Id: <employee uuid>. - Auth resolves the key to
whatsapp-connector, confirms the ceiling permitsusers:impersonate, loads the employee, and computes effective scopes as(employee's role scopes ∩ ceiling) + self-read. - 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.