Format, Storage, and Lifecycle
The plaintext credential shape, the stored metadata, and what changes as a key ages.
Athena separates the credential you hand to a caller from the fields it stores
in Postgres. The authoritative model lives in ApiKeyRecord,
get_api_key_by_public_id, and create_api_key.
Plaintext key format
New keys are issued as:
ath_{public_id}.{secret}The current implementation in src/api/admin/mod.rs generates:
public_id: the first 16 hex characters of a UUID v4 stringsecret: two UUID v4 strings concatenated together
That means a newly created key looks like a short lookup prefix plus a long secret, for example:
ath_abcd1234efab5678.59f0d9f7d5e44f86a0d6d488c2b8d0a94b6b3b4b4b4f4a3b86d651a1f0f048cThe full plaintext key is returned once during POST /admin/api-keys and is
not retrievable later. Athena stores the lookup and verification material, not
the secret itself.
What Athena stores
The main persisted fields are:
| Field | Purpose |
|---|---|
id | External UUID returned by admin APIs |
public_id | Short lookup identifier embedded in the plaintext key |
client_name | Optional binding to one logical X-Athena-Client |
key_salt | Per-key salt used during verification |
key_hash | SHA-256 digest of key_salt:secret |
is_active | Soft lifecycle toggle |
expires_at | Hard expiration cutoff |
last_used_at | Updated asynchronously after auth attempts |
virgin_mode and related fields | Lock-in and IP learning state |
Athena also stores right grants in api_key_right_grants, reusable right
definitions in api_key_rights, and optional IP policy rows in
api_key_ip_whitelist, api_key_ip_blacklist, and api_key_ip_seen.
Validation lifecycle
At runtime, authorize_gateway_request does not compare the caller's secret to
stored plaintext. It:
- Extracts the
public_idandsecretfromX-Athena-Key. - Loads the stored row with
get_api_key_by_public_id. - Recomputes the expected digest from
key_saltand the presented secret. - Rejects or continues based on active state, expiration, client binding, rights, and IP policy.
Lifecycle states
Athena treats a key as valid only when all of these conditions pass:
- The key has the right
ath_{public}.{secret}shape. public_idresolves to a stored row.- The recomputed hash matches
key_hash. is_activeis true.expires_atis unset or in the future.- If
client_nameis set, it matches the request'sX-Athena-Client. - The key has all required rights.
- The IP policy allows the caller.
The most common state changes are:
| Change | Result |
|---|---|
PATCH /admin/api-keys/{id} with is_active: false | Runtime auth starts returning Inactive API key |
| Expiration time passes | Runtime auth starts returning Expired API key |
DELETE /admin/api-keys/{id} | Lookup by public_id stops finding the record |
| Successful auth | last_used_at is touched asynchronously |
Create response shape
curl -X POST "http://localhost:4052/admin/api-keys" \
-H "X-Athena-Admin-Key: $ATHENA_KEY_12" \
-H "Content-Type: application/json" \
-d '{
"name": "analytics-worker",
"client_name": "analytics",
"rights": ["gateway.query"]
}'Typical response fields:
{
"status": "success",
"message": "Created API key",
"data": {
"api_key": "ath_abcd1234efab5678.secret",
"record": {
"id": "uuid",
"public_id": "abcd1234efab5678",
"client_name": "analytics",
"is_active": true,
"rights": ["gateway.query"]
}
}
}Constraints to remember
POST /admin/api-keysrequires the static admin secret, not a gateway key.- Keys are currently looked up by
public_id, so exposing the prefix is fine; the secret still must matchkey_hash. - Right names must already exist in
api_key_rights, or creation/update fails. - Runtime gateway auth currently reads
X-Athena-Keyviaextract_gateway_api_key.