Skip to content

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:

  1. Service eligibility — which upstream APIs this token can access (default deny)
  2. Usage quota — requests per hour and per day (default unlimited)
  3. 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 scopes field (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 (or unlimited)
  • X-Quota-Remaining-Day — remaining daily quota (or unlimited)
  • 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.