How obleth models tenants, API keys, fairshare groups, and quotas, and how they relate to each other.
obleth's identity model has four layers: fairshare groups, tenants, API keys, and quotas. Understanding how they fit together helps you design the right configuration for your use case.
A tenant is the unit of fairshare. Every API key belongs to exactly one tenant, and fairshare weight is applied at the tenant level.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "chatbot",
"fairshare_group": "prod",
"weight": 500,
"tokens_per_minute": 2000000,
"max_in_flight": null
}
| Field | Purpose |
|---|---|
name | Human-readable identifier. Unique. |
weight | Fairshare priority. Higher = larger share under contention. Min 1. Default 100. |
tokens_per_minute | Sustained token budget (rate limit). The token bucket refills at this rate. |
max_in_flight | Optional per-tenant concurrency cap. null means only the global limit applies. |
fairshare_group | The fairshare group this tenant belongs to (defaults to "default"). |
Create a tenant:
curl -X POST http://localhost:9090/api/v1/tenants \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "chatbot",
"weight": 500,
"tokens_per_minute": 2000000
}'
API keys are the authentication credential clients use on the data plane. Each key belongs to one tenant.
{
"id": "...",
"tenant_id": "550e8400-...",
"name": "prod",
"key_prefix": "sk_a1b2c3d4e5f6a1b",
"disabled": false,
"created_at": "2026-01-15T10:00:00Z"
}
The raw secret (e.g. sk_a1b2c3d4...48hexchars) is returned once at creation and never stored. Only a SHA-256 hash is kept. If you lose the secret, rotate the key (delete and create a new one).
A tenant can have many keys (e.g. one per environment: prod, staging, ci). All keys for the same tenant share the same weight and quota.
Create a key:
curl -X POST http://localhost:9090/api/v1/tenants/$TID/keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "prod"}'
# Returns: {"key": {...}, "secret": "sk_..."}
Disable a key (useful for rotation without deleting):
curl -X PUT http://localhost:9090/api/v1/keys/$KEY_ID/disabled \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"disabled": true}'
Groups are optional. Every tenant is in the "default" group unless you create and assign one.
Groups are only meaningful under the hierarchical fairshare algorithm. Under weighted, groups are ignored and all tenants compete globally.
Create a group:
curl -X POST http://localhost:9090/api/v1/fairshare/groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "prod", "weight": 500}'
Assign a tenant to a group:
curl -X PATCH http://localhost:9090/api/v1/tenants/$TID/group \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"fairshare_group": "prod"}'
Under hierarchical, the prod group (weight 500) will receive a proportionally larger slice of global capacity than the default group (weight 100), regardless of how many tenants are in each.
Quotas operate at the tenant level and are enforced per-pod via Redis token buckets:
tokens_per_minute: the token-bucket refill rate. A tenant with tokens_per_minute=60000 can sustain 1000 tokens/second. The burst ceiling equals the same value.max_in_flight: an optional hard cap on concurrent requests for this tenant. If set, a tenant can never hold more than this many permits simultaneously, regardless of the global limit.Update quota:
curl -X PUT http://localhost:9090/api/v1/tenants/$TID/quota \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tokens_per_minute": 4000000, "max_in_flight": 20}'
All quota changes propagate to Redis and are picked up on the next request.
On the data plane, everything reduces to a ResolvedKey — the compact hot-path representation cached in moka and Redis:
{
"key_id": "...",
"tenant_id": "...",
"tenant_name": "chatbot",
"fairshare_group": "prod",
"group_weight": 500,
"weight": 500,
"tokens_per_minute": 2000000,
"max_in_flight": null,
"disabled": false
}
This is the only thing the data plane reads. It carries everything fairshare admission needs — no relational lookup on the hot path.