Files
goods/migrations/0008_api_key.up.sql
T
novaalphastrikeomegaz663 2820823b36
CI / Python (ingestion) (pull_request) Successful in 12s
CI / Migrations (postgres) (pull_request) Successful in 24s
CI / Go (api) (pull_request) Successful in 53s
feat(api): API keys + Redis rate limiting + usage stats
Add an optional API-key layer to the public read-only API. Keys grant
higher per-minute rate limits and attribute usage; anonymous callers are
still allowed at a lower IP-based budget.

- migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once)
- apikey pkg: key generation + hashing
- ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open
- public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After
- admin: issue/list/revoke keys + usage view (API + UI tab)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 08:24:07 +00:00

25 lines
1.3 KiB
SQL

-- API keys for the public read-only API. Keys grant higher rate limits and let
-- usage be attributed to a caller; the API itself stays free and read-only.
-- Only the SHA-256 hash of a key is stored; the plaintext is shown once at
-- creation time. Keys are issued/revoked from the admin console. The public
-- server only ever SELECTs from this table (request counting lives in Redis),
-- preserving its read-only contract against PostgreSQL.
CREATE TABLE IF NOT EXISTS api_key (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
key_prefix VARCHAR(20) NOT NULL, -- shown for identification, e.g. og_live_AbC1
key_hash TEXT NOT NULL UNIQUE, -- hex SHA-256 of the full key
owner_email TEXT,
tier VARCHAR(16) NOT NULL DEFAULT 'free',
rate_limit_per_min INT NOT NULL DEFAULT 120,
revoked_at TIMESTAMPTZ,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT api_key_tier_chk CHECK (tier IN ('free', 'partner', 'internal')),
CONSTRAINT api_key_rate_chk CHECK (rate_limit_per_min > 0)
);
-- Fast lookup of active keys by their hash on every authenticated request.
CREATE INDEX IF NOT EXISTS idx_api_key_active_hash
ON api_key (key_hash) WHERE revoked_at IS NULL;