# App users (/architecture/app-users)



Apps sign their own end users up and in through `db.auth`, and the data API runs their requests as `authenticated` with the user's claims. For how to enable and use app users, see the [app users guide](/using/app-users). This page covers the moving parts.

## Who holds what [#who-holds-what]

* **The identity service holds the identities**, in one pool per project (`app-<project id>`, its own Postgres schema; see [Identity service](/architecture/identity#pools)). It checks passwords and is the source of truth for who exists. The worker creates the pool when app users are enabled and drops it when app users are disabled (unless it still holds users) or the project is deleted.
* **Chelabase issues the tokens.** The `auth` service checks the password against the project's pool, which opens an identity session (90 days), then signs a short-lived EdDSA access token (`iss` is `chelabase-auth`, `aud` is `chelabase-project:<project id>`) and a rotating refresh token bound to that identity session. The flows live under `/auth/v1/app/` and take a public key.
* **Your database holds a users table** that you own, in one chosen auth database of the project: one row per user, on every branch of that database.
* **The orchestrator keeps the sessions.** Sessions live in the metadata database, which stores only the SHA-256 of each refresh token. The orchestrator rotates refresh tokens, detects reuse and revokes sessions.

## Request path [#request-path]

An app request carries `Authorization: Bearer <public key>` plus `Chelabase-User: <access token>`. The gateway verifies the token with the app-user public key and checks that its session is still live (cached 30 seconds, so a revocation lands within 30 seconds). It then mints the identity token for `rest` with the user's claims, and `rest` runs the request as `authenticated` with `request.jwt.claims` set. Policies read the claims through the `chelabase.claims()`, `chelabase.user_id()` and `chelabase.user_email()` helpers. See [Gateway](/architecture/gateway) for the full check.

The token key pair is made once by `init-secrets` (`auth/app-jwt.key` and `app-jwt.pub` in the `secrets` volume; `.chelabase/auth/` on a workbench stack), never overwritten, and separate from the member-token pair: `auth` signs, the gateway verifies. A public JWKS endpoint (`GET /auth/v1/app/jwks`) lets your own servers verify tokens too.

## Sync and reconcile [#sync-and-reconcile]

The identity pool is the source of truth. Two paths copy users into the table on every live branch of the auth database:

* **Write-through.** Every change Chelabase makes to a user (sign-up, an email change, a ban, a delete, through the app flows or the platform admin) is made in the pool first, then publishes a sync job for that user. An email change, a password change or a deactivation also ends the user's identity sessions inline, so their refresh tokens stop working.
* **Reconcile.** Every `CHELABASE_APP_USERS_RECONCILE_SECS` (default 300) per enabled project, and right after a branch of the auth database is created, the worker compares the pool's users with each branch's rows and syncs every difference. The same pass reruns enables, disables and project deletes that have been stuck for 5 minutes, and purges sessions that were revoked or expired more than 7 days ago.

A deactivated user keeps their row, with `banned_at` set to when the sync first saw the ban.

The sync writes rows as the `chelabase_auth_sync` role. A guard trigger refuses inserts, deletes and changes to managed columns from any other role, so rows are Chelabase's to create and delete.

## Settings [#settings]

| Variable                                                         | Default                                                                                                                                       | Read by                                                                                                                                                                                                                                                                                    |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CHELABASE_IDENTITY_URL`, `CHELABASE_IDENTITY_TOKEN_FILE`        | none (both or neither); Compose sets `http://identity:8080` and each caller's `tokens/<caller>-identity.token`                                | orchestrator, worker, auth (pool and user operations). Auth won't start without them. When the identity service can't be reached, the management API's app-user routes answer 503 `identity_unavailable`, the `/auth/v1/app/*` flows answer 503 `unavailable`, and the worker's jobs wait. |
| `CHELABASE_APP_JWT_PRIVATE_KEY` / `CHELABASE_APP_JWT_PUBLIC_KEY` | self-hosted: `auth/app-jwt.key` / `.pub` in the `secrets` volume; workbench: the stack's `stack.env` (`.chelabase/auth/app-jwt.key` / `.pub`) | auth signs, the gateway verifies                                                                                                                                                                                                                                                           |
| `CHELABASE_INTERNAL_VERIFY_KEY`, `CHELABASE_COMPUTE_CA_FILE`     | set by Compose                                                                                                                                | auth. Without them, as without the app-JWT keys, app users are off.                                                                                                                                                                                                                        |
| `CHELABASE_APP_AUTH_INTERNAL_TOKEN`                              | `chelabase-dev-app-auth`                                                                                                                      | auth to the orchestrator's internal app routes                                                                                                                                                                                                                                             |
| `CHELABASE_APP_SESSIONS_INTERNAL_TOKEN`                          | `chelabase-dev-app-sessions`                                                                                                                  | gateway to session liveness                                                                                                                                                                                                                                                                |
| `CHELABASE_APP_ACCESS_TTL_SECS`                                  | `900` (60 to 86400)                                                                                                                           | auth                                                                                                                                                                                                                                                                                       |
| `CHELABASE_APP_REFRESH_TTL_SECS`                                 | `2592000` (30 days; at most 90 days)                                                                                                          | orchestrator                                                                                                                                                                                                                                                                               |
| `CHELABASE_APP_USERS_RECONCILE_SECS`                             | `300` (30 to 86400)                                                                                                                           | worker                                                                                                                                                                                                                                                                                     |
| `CHELABASE_INTERNAL_URL`                                         | `http://127.0.0.1:8083`                                                                                                                       | auth (the orchestrator's internal listener)                                                                                                                                                                                                                                                |

The default internal tokens are public development values: every service refuses them unless `CHELABASE_DEV_DEFAULTS=1` (the workbench sets it). A Compose install reads generated tokens from `tokens/app-auth.token` and `tokens/app-sessions.token`.

## Tests [#tests]

`just app-users-e2e` (runs on your checkout's stack) runs the whole story live: enabling app users, sign-up and sign-in, row-level security on the users table, branches receiving the rows, an email change reaching the row, refresh rotation and reuse detection, and project deletion dropping the pool. `app_users_live` (`just workbench test -p worker --test app_users_live -- --run-ignored only`) covers the bootstrap, the guard and the worker's enable, sync, resync and disable against a real compute and the identity service.
