App users
How app users are stored, authenticated and kept in sync between the identity service and your database.
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. This page covers the moving parts.
Who holds what
- The identity service holds the identities, in one pool per project (
app-<project id>, its own Postgres schema; see Identity service). 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
authservice checks the password against the project's pool, which opens an identity session (90 days), then signs a short-lived EdDSA access token (issischelabase-auth,audischelabase-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
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 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
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
| 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
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.