-- The tenancy tree. Three levels, each owning the next. CREATE TABLE organizations ( id TEXT PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE teams ( id TEXT PRIMARY KEY, org_id TEXT NOT NULL REFERENCES organizations(id), name TEXT NOT NULL ); CREATE INDEX teams_by_org ON teams(org_id); CREATE TABLE projects ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL REFERENCES teams(id), name TEXT NOT NULL ); CREATE INDEX projects_by_team ON projects(team_id); -- Only sha256(key) is stored, so a database leak hands over no working -- credential. SHA-256 rather than a slow password hash on purpose: the input is -- 256 random bits with no dictionary to attack, so the cost of argon2 would buy -- nothing and charge tens of milliseconds on every request (plan ยง7.9.1). CREATE TABLE api_keys ( id TEXT PRIMARY KEY, key_hash BLOB NOT NULL UNIQUE, key_prefix TEXT NOT NULL, name TEXT NOT NULL, project_id TEXT NOT NULL REFERENCES projects(id), band TEXT, routing_profile TEXT, created_at INTEGER NOT NULL, expires_at INTEGER, -- Revocation is a soft delete. Usage rows reference the key, and an audit -- trail that loses its subject is not an audit trail. revoked_at INTEGER, last_used_at INTEGER ); CREATE INDEX api_keys_by_project ON api_keys(project_id); -- Limits hang off any scope rather than being duplicated onto each one. A NULL -- column means "no limit at this level", which is not the same as 0. CREATE TABLE limits ( scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, rpm INTEGER, tpm INTEGER, -- Micro-units, not a float: a budget is accumulated by repeated addition, and -- repeated addition of floats drifts. budget_micros INTEGER, budget_period TEXT, allowed_models TEXT, PRIMARY KEY (scope_type, scope_id) ); -- One row per request, written in batches off the request path. CREATE TABLE usage ( ts INTEGER NOT NULL, key_id TEXT NOT NULL, project_id TEXT NOT NULL, model TEXT NOT NULL, served_model TEXT NOT NULL, prompt_tokens INTEGER NOT NULL, completion_tokens INTEGER NOT NULL, cost_micros INTEGER NOT NULL, status INTEGER NOT NULL, latency_ms INTEGER NOT NULL ); -- Budget restore reads by key within a period, which is this index exactly. CREATE INDEX usage_by_key_ts ON usage(key_id, ts); CREATE INDEX usage_by_ts ON usage(ts);