Docs
Registration & API keys
Access is a tenant plus one or more API keys. Keys are keyID.secret. The secret is returned once. The database stores an HMAC-SHA256 of the full key using the server master key (internal/crypto.HashAPIKey). Verification is constant-time (hmac.Equal).
bcrypt is used for passwords (kanban / DB credential hashing helpers), not for API keys.
Bootstrap token
Production will not start without AH_BOOTSTRAP_TOKEN unless you pass --dev --open-registration.
- Minimum length 32 characters per token
- Comma-separated list is allowed:
AH_BOOTSTRAP_TOKEN=new,old— both work during rotation - Compared with HMAC (no length leak) in
validateBootstrapToken - Sent as header
X-Bootstrap-Token(notAuthorization)
Validate without registering:
curl -s -X POST "$AH_URL/v1/system/bootstrap-token/validate" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$AH_BOOTSTRAP_TOKEN\"}"
# → 200 {"valid":true} or 200 {"valid":false} — never 401 for a bad token
The token is in the JSON body, not X-Bootstrap-Token (that header is used by register). Recover / reactivate / quota patch take X-Admin-Token when AH_ADMIN_TOKEN is set.
Admin token
AH_ADMIN_TOKEN (comma-separated, 32+ chars) authorizes operator-only routes: POST /v1/auth/recover, POST /v1/tenants/{id}/reactivate, PATCH /v1/tenants/{id}/quotas. Send X-Admin-Token (or the same value in X-Bootstrap-Token). Once configured, a valid bootstrap token on those routes returns 403. If unset, bootstrap tokens still work (deprecated; startup warning).
Register
POST /v1/tenants/register
X-Bootstrap-Token: <token>
{"name":"my-agent","email":"agent@example.com"}
Rules from handleTenantRegister:
- Name 2–128 characters
- Email must match
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$, max 256 - Max 1000 active tenants
- Duplicate name/email → 422 (
registration failed, please check your details and try again) - Rate limit: 5 / IP / hour and 20 / hour global (same limiter as recover, reactivate, quota patch, bootstrap validate)
Response:
{"tenant_id":"<hex>","api_key":"<keyID>.<secret>"}
--dev --open-registration skips the bootstrap header. Anyone who can reach the port can create tenants.
Authenticated requests
Authorization: Bearer <keyID>.<secret>
Auth middleware (internal/middleware/auth.go):
- Looks up the key by ID, checks not revoked / not expired / tenant
active - Verifies HMAC
- Caches the lookup 30 seconds (max 5000 entries) so revocation can lag up to 30s
- Samples
last_used_atat most every 5 minutes
Suspended tenants cannot authenticate.
Create, list, revoke
POST /v1/auth/keys {"name":"ci","expires_in":2592000}
GET /v1/auth/keys
DELETE /v1/auth/keys/{keyID}
namedefaults tounnamedexpires_inis seconds, must be > 0, max ~10 years- Response field is
api_key(notkey) - List never returns the secret; it returns
id,name,prefix,created_at,last_used_at,expires_at - Max 20 non-revoked keys per tenant → 409
quota exceeded: maximum API keys reached...
Creating a key invalidates the auth cache for that tenant.
Recover lost keys
When every key is gone:
POST /v1/auth/recover
X-Admin-Token: <admin-token>
{"email":"agent@example.com"}
Email must match the tenant. Issues a new key. Same registration rate limiter.
Reactivate a suspended tenant
DELETE /v1/tenant soft-deletes: status becomes suspended and services/DBs/kanbans/builds are stopped.
POST /v1/tenants/{tenantID}/reactivate
X-Admin-Token: <admin-token>
Returns {"tenant_id","api_key","status"}. Already-active tenants return 409. Rate-limited like registration.
Tenant self-service
| Method | Path | Notes |
|---|---|---|
| GET | /v1/tenant | id, name, email, status, timestamps |
| PATCH | /v1/tenant | name only (2–128 chars). Email is not updatable here. |
| DELETE | /v1/tenant | Suspend + stop resources |
| GET | /v1/tenant/usage | Used/max for services, databases, API keys; memory/cpu/disk/rate_limit quotas |
| GET | /v1/tenant/usage/metrics | Metering rollups (period=hourly|daily, since/until RFC3339) |
Operator quota changes:
PATCH /v1/tenants/{tenantID}/quotas
X-Admin-Token: <admin-token>
Same bootstrap rate limiter.
ahc equivalents
ahc register --name my-agent --email agent@example.com --save
ahc key create --name ci
ahc key list
ahc key revoke <keyID>
ahc recover --email agent@example.com
Config file expected by ahclient.LoadConfig is ~/.ah/config.json with url and key.