Token Management¶
Philosophy¶
Tokens are credentials, not identities. A person can have multiple tokens for different contexts (local dev, CI, shared bot). There are no groups, roles, or RBAC — just flat tokens with three constraints:
- Service eligibility — which upstream APIs this token can access (default deny)
- Usage quota — requests per hour and per day (default unlimited)
- Expiration — when the token stops working (default never)
Every token answers: who is this for, what can it do, how much can it use, and when does it end.
Token Lifecycle¶
create → use → rotate (or) revoke
↓
old token enters grace period → auto-expires
new token issued
- Create: Admin issues a token. The raw value is shown once and never stored.
- Use: Token is validated via SHA-256 hash lookup (KV cache → D1 fallback).
- Rotate: New token inherits the old token's config. Old token stays valid for a grace period (default 7 days), then expires.
- Revoke: Hard kill. Token is immediately invalid.
Policy Model¶
Permissions are stored as policy_json on the token:
{
"services": {
"exa": true,
"firecrawl": true,
"groq": true
}
}
- Default deny. If a service isn't listed, the token can't use it.
- No read/write distinction — upstream APIs are almost universally POST-based, so HTTP method gating doesn't map to intent.
- Legacy
scopesfield (comma-separated service names or*) is still honored as a fallback for pre-migration tokens.
Quotas¶
Two counters, both optional (null = unlimited):
| Field | Window | KV Key Pattern | TTL |
|---|---|---|---|
quota_rph |
Per hour | q:h:{tokenId}:{YYYY-MM-DDTHH} |
2h |
quota_rpd |
Per day | q:d:{tokenId}:{YYYY-MM-DD} |
2d |
Counters are global across all services (not per-service). When both are null, KV is never touched — zero overhead for unlimited tokens.
KV counters are eventually consistent, so quotas are best-effort. At this team size, that's fine.
Admin API¶
| Method | Endpoint | Description |
|---|---|---|
POST |
/admin/tokens |
Create token (returns raw value once) |
GET |
/admin/tokens |
List tokens (supports ?q=, ?active=, ?service=) |
GET |
/admin/tokens/:id |
Token detail |
PATCH |
/admin/tokens/:id |
Update name, services, quotas, expiry |
POST |
/admin/tokens/:id/rotate |
Rotate with grace period |
DELETE |
/admin/tokens/:id |
Hard revoke |
Create Token¶
curl -X POST https://keypool.lvtu.in/admin/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member_name": "alice",
"token_name": "CI: main repo",
"services": ["exa", "firecrawl"],
"quota_rpd": 5000,
"expires_at": "2026-06-01T00:00:00Z"
}'
Rotate Token¶
curl -X POST https://keypool.lvtu.in/admin/tokens/3/rotate \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"grace_days": 7}'
Self-Inspection¶
Token holders can check their own permissions without admin access:
curl https://keypool.lvtu.in/whoami \
-H "Authorization: Bearer $MY_TOKEN"
Returns: token name, prefix, member name, service permissions, quotas, expiry, and last use.
Response Headers¶
Every proxied response includes:
X-Quota-Remaining-Hour— remaining hourly quota (orunlimited)X-Quota-Remaining-Day— remaining daily quota (orunlimited)X-KeyPool-Key-Id— which upstream key was used
Schema¶
See schemas/d1-schema.sql for the base schema and schemas/002-token-management.sql + schemas/003-quota-columns.sql for migrations.