# API Usage Quota & UI

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 94  
**Tier:** Business+  
**Depends on:** `white-label-api`, `api-keys-ui`, `foundation-auth-rbac`  
**Referenced by:** `white-label-api`, `api-keys-ui`

---

## Overview

Spec 27 (`white-label-api`) defines `tenant_api_keys` with scopes and last_used_at, but has no per-key request counting or quota enforcement. Spec 54 (`api-keys-ui`) adds the CRUD UI. This spec adds: per-key monthly request counters stored in Cloudflare Analytics Engine, per-key quota limits, UI usage charts, and quota-exceeded gate.

---

## Data Model

```sql
-- Schema delta on tenant_api_keys:
ALTER TABLE tenant_api_keys ADD COLUMN monthly_quota INTEGER;
-- NULL = unlimited. Enforced per calendar month (UTC).
```

Usage is tracked in **Cloudflare Analytics Engine** (not Postgres) for high-frequency inserts:

```ts
// Written on every authenticated API request:
env.API_USAGE.writeDataPoint({
  indexes: [tenantId],
  blobs:   [keyId, endpoint, method],
  doubles: [1],  // request count
})
```

AE dataset: `api_usage` — queried with SQL aggregation for daily/monthly rollups.

---

## Quota Enforcement

In `apiKeyAuthMiddleware` (spec 27), after key lookup:

```ts
if (apiKey.monthly_quota !== null) {
  const used = await getMonthlyUsage(tenantId, keyId)  // AE query
  if (used >= apiKey.monthly_quota) {
    return c.json({ error: 'quota_exceeded', limit: apiKey.monthly_quota, used }, 429)
  }
}
```

`getMonthlyUsage`: AE query for current UTC calendar month, sum of doubles for this keyId.

---

## Settings UI: `/settings/api`

The API usage & quota dashboard (distinct from key management at `/settings/api-keys`, spec 60). Lists every key with a usage indicator; key CRUD/scopes live on spec 60's page. Each key row:

```
┌──────────────────────────────────────────────────────────────┐
│  API Keys                                      [+ New key]   │
│                                                              │
│  Name              Prefix       Last used   Usage / Quota    │
│  ──────────────────────────────────────────────────────────  │
│  Zapier            zyk_live     2 days ago  1,240 / 5,000   │
│                                            ████░░░░ 24.8%   │
│  GitHub CI         zyk_live     1 hour ago  892 / unlimited  │
│  Internal sync     zyk_test     Never       0 / 1,000        │
│                                                              │
│  Usage resets monthly (UTC)                                  │
└──────────────────────────────────────────────────────────────┘
```

Progress bar: green <70%, amber 70-90%, red >90%, grey if unlimited.

---

## Key Detail: Usage Chart

`/settings/api/:id`:

```
┌──────────────────────────────────────────────────────────────┐
│  Zapier integration                           [Edit] [Revoke] │
│                                                              │
│  Prefix: zyk_live  Scopes: customers:read, invoices:read     │
│  Quota: 5,000 / month                                        │
│                                                              │
│  ── This month ──────────────────────────────────────────── │
│  1,240 requests used  |  3,760 remaining                     │
│                                                              │
│  Daily requests (last 30 days):                              │
│  │████│███│██│█████│███│  ← bar chart                       │
│  Mon  Tue  Wed  Thu  Fri                                     │
│                                                              │
│  Top endpoints this month:                                   │
│  GET /v1/customers    842 (67.9%)                            │
│  GET /v1/invoices     310 (25.0%)                            │
│  POST /v1/leads        88 (7.1%)                             │
└──────────────────────────────────────────────────────────────┘
```

---

## Edit Key Modal

Sets the monthly quota on an existing key. Name and scopes are set at creation on spec 60's key manager (`/settings/api-keys`) — this modal, reached from the usage dashboard, owns only the quota:

```
┌──────────────────────────────────────────────────────────────┐
│  Edit API key quota — Zapier integration                     │
│                                                              │
│  Monthly quota                                               │
│  ● Unlimited                                                 │
│  ○ Limit to  [5000___] requests/month                        │
│                                                              │
│  [Cancel]                            [Save changes]          │
└──────────────────────────────────────────────────────────────┘
```

---

## API

```
GET /api/api-keys/:id/usage
    → monthly usage summary + daily breakdown (last 30 days)
      query: month? (default: current UTC month)
      Returns: { used, quota, remaining, daily: [{date, count}], topEndpoints: [{path, count}] }
      Requires: OWNER, ADMIN (Business+)

PATCH /api/api-keys/:id/quota
      → set the monthly quota on a key (name/scopes are managed by spec 60)
        body: { monthly_quota?: number | null }   -- null = unlimited
        Requires: OWNER (Business+)
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| AE for usage tracking | Not Postgres counter | API requests are high-frequency; AE handles millions of writes/day. **Trade-off:** AE queries are eventually consistent (writes async, query lag ~1-2min); quota gate is best-effort near the boundary — a brief overage is possible. Acceptable for soft quota; would need transactional counter for hard billing limit | without Postgres connection pressure; per-request insert to Postgres would not scale |
| Calendar month reset | Not rolling 30 days | Simpler UX (resets on 1st), easier to communicate to customers |
| Quota in Postgres | Not AE | Quota is configuration, not telemetry; needs transactional update |
| 429 on quota exceeded | Not 403 | RFC 6585 / convention for rate-limit errors; `Retry-After` header indicates month reset date |
