Authentication
ChatWalaʻau has two complementary auth mechanisms: API keys (Bearer tokens you issue per caller) and an optional web sign-in (username/password) for cloud deployments.
API_KEY and RESPONSES_API_KEY in .env are no longer read. A value still set is named in a startup
warning and ignored. Callers that used them get 401 until you give them a managed key (below). Old values are
not imported.
API keys
Every caller gets its own key. A key has a name, one scope, an optional expiry and an enabled switch.
| Scope | May call | Give it to |
|---|---|---|
responses | POST /v1/responses only | a company site's server, an external app |
api | every API except key management (/api/*, /ag-ui/, WebSockets, /v1/responses) | the CLI, automation |
admin | everything, including key management | only where keys are managed |
Create a key
- API Keys screen: the key icon in the sidebar footer (wide screens). It appears when you are signed in through the browser or use ChatWalaʻau on the same machine. Issue, edit, disable / enable, reissue and delete keys there; each action shows a spinner until it is done.
- CLI on the server (also how you make the first key when there is no web sign-in):
chatwalaau keys create --name company-site-bff --scope responses --expires-days 90
chatwalaau keys list
chatwalaau keys disable <id> # keep it, but refuse it
chatwalaau keys enable <id>
chatwalaau keys reissue <id> # a new secret; the old one stops working
chatwalaau keys delete <id>
The secret (cwk_<id>_...) is shown once, when it is issued or reissued. Only its SHA-256 hash is stored, in
the file named by API_KEYS_FILE, with an audit log of every change beside it:
API_KEYS_FILE=api_keys.jsonc # default; on Azure App Service use a path under /home
# APP_REQUIRE_AUTH_ON_LAN=true # default: fail-closed on LAN without a key
Changes apply on the next request, without a restart. api_keys.jsonc and its audit log are ignored by Git,
Docker builds and the public repository sync.
What is accepted where
Same-machine clients (127.0.0.1, ::1, localhost) pass the write endpoints and the AG-UI stream without a
key, so localhost development stays zero-configuration even when APP_HOST=0.0.0.0.
| Client address | APP_REQUIRE_AUTH_ON_LAN | Active key or web sign-in | Outcome on /api/* and POST /ag-ui/ |
|---|---|---|---|
| loopback | any | any | allow |
| LAN | false | any | allow (operator opt-out) |
| LAN | true | neither | 503 |
| LAN | true | yes | an api / admin key (or the sign-in cookie) required |
/v1/responsesalways requires a key, of any scope, regardless of client address (503 when no key is active).- An unknown, expired or disabled key gets
401; a key whose scope is too narrow gets403. - Key management accepts only web sign-in, the same machine or an
adminkey -- never theAPP_REQUIRE_AUTH_ON_LAN=falseopen mode -- and a caller cannot grant a scope above its own. APP_REQUIRE_AUTH_ON_LANstays in.env; the API Keys screen shows it read-only.
Per-key limits
Since v0.216.0 a key can carry its own /v1/responses limits, set in the API Keys screen under
Limits for /v1/responses:
| Field | Meaning |
|---|---|
requests_per_minute / requests_per_day / requests_per_month | Requests this key may make |
total_tokens_per_day / total_tokens_per_month | Input plus output tokens this key may spend |
Empty or 0 means no limit for this key. The global limits in App Settings apply as well
(usage limits). The screen shows each key's usage today and
this month. A key over its limit gets 429 with a key_ code. The other keys keep working, so one leaked
key cannot use up the whole budget. Over HTTP, send "limits": {...} with POST or PATCH /api/api-keys; null clears them.
Agent and conversations per key
Since v0.218.0 a key can be bound to a custom agent and choose how its /v1/responses conversations are stored,
in the API Keys screen under Agent and conversations for /v1/responses:
| Field | Meaning |
|---|---|
| Agent | None (the request chooses the model), or a custom agent: the key then runs only that agent |
| Conversations | Saved (listed in the chat history), or Temporary (never listed, deleted after the retention period) |
The list marks strict agents (documents only) and agents with problems (has problems); a key bound to an agent
with problems gets 503 until it is fixed. Over HTTP, send "agent" (null unbinds) and "storage" with POST
or PATCH /api/api-keys. From the server: chatwalaau keys set ID --agent hr-desk --storage temporary
(--no-agent unbinds). See Document desk.
Upgrading to v0.215.0
- Upgrade. Callers using
API_KEY/RESPONSES_API_KEYnow get401. - Optionally set
API_KEYS_FILE(under/homeon Azure App Service). - Create one key per caller:
responsesfor a BFF or SDK app,apifor the CLI (CHATWALAAU_API_KEY) and automation,adminonly where keys are managed. - Remove
API_KEYandRESPONSES_API_KEYfrom.envand your platform configuration.
A responses key replaces RESPONSES_API_KEY with the same guarantee: its holder can call the API but cannot
change the settings that bound it (operator controls). Requests
from a loopback peer still pass /api/* without a key, so run such a caller on another host.
Web SPA authentication (optional)
For deploying ChatWalaʻau as a private cloud web app where a single operator signs
in through the browser. It coexists with API keys (still used for CLI / SDK
access) and is disabled by default -- without AUTH_USERNAME there is no
behavior change.
AUTH_USERNAME=admin
AUTH_PASSWORD_HASH=scrypt$N=16384,r=8,p=1$<base64-salt>$<base64-hash>
# AUTH_SESSION_TTL_SECONDS=86400 # default 24h, sliding
# AUTH_COOKIE_SECURE=auto # auto / true / false
# AUTH_COOKIE_NAME=chatwalaau_session
# AUTH_SESSION_PERSIST=true # sessions survive a restart (default)
# AUTH_SESSION_STORE_PATH=.auth/session_tokens.json
Generate the hash with the bundled CLI:
chatwalaau hash-password # interactive (confirms twice)
echo "$PASSWORD" | chatwalaau hash-password --stdin --quiet # scripted
When AUTH_USERNAME is set, the SPA renders a /login page; the server validates
credentials in constant time and issues an opaque token via an HttpOnly +
SameSite=Strict cookie. The backend then accepts either a Bearer API key (scope api or admin)
or a valid session cookie on every write endpoint and the AG-UI stream. The
/v1/responses external-app path stays Bearer-only.
- No new Python dependency (stdlib
hashlib.scrypt+secrets) - Single-user, single-process model
- HTTPS strongly recommended for non-loopback deployments
- Loopback CLI calls keep their no-credential bypass
Sessions survive a restart
Since v0.104.0, signing in survives a backend restart: a deploy, a crash, or a dev-server
relaunch no longer sends every browser back to the login page. The server stores only the
SHA-256 digest of each session token in AUTH_SESSION_STORE_PATH -- never the token
itself -- so the file cannot be turned into a working cookie and needs no encryption. It
is written atomically, is 0600 on POSIX, and is git-ignored.
To sign every session out, pick whichever fits your workflow:
| Action | Effect |
|---|---|
Delete AUTH_SESSION_STORE_PATH | All sessions end at the next restart |
Rotate AUTH_PASSWORD_HASH | All sessions end immediately at the next restart |
AUTH_SESSION_PERSIST=false | Restores the old behavior: sessions die with the process |
Restarting the process alone no longer signs users out.
When a session does expire
If a session expires (TTL, logout elsewhere, a rotated password), the app does not
reload to /login. It opens a sign-in dialog over your intact chat, so the message you
were typing, your attachments, and your model selection are all preserved. Signing in
returns you exactly where you were. The /login page is still used at first load and
after an explicit sign-out.
Likewise, if the server is unreachable when you press Send, your message is returned to the input box with a Retry button rather than being discarded.