-- Migration: Access Management Schema -- Description: Adds RPC product subscriptions, richer API key metadata, and usage logging. ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS product_slug VARCHAR(100), ADD COLUMN IF NOT EXISTS scopes TEXT[] DEFAULT ARRAY[]::TEXT[], ADD COLUMN IF NOT EXISTS monthly_quota INTEGER, ADD COLUMN IF NOT EXISTS requests_used INTEGER NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS approved BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP, ADD COLUMN IF NOT EXISTS approved_by VARCHAR(255), ADD COLUMN IF NOT EXISTS last_ip_address INET; CREATE TABLE IF NOT EXISTS rpc_products ( slug VARCHAR(100) PRIMARY KEY, name VARCHAR(255) NOT NULL, provider VARCHAR(100) NOT NULL, vmid INTEGER NOT NULL, http_url TEXT NOT NULL, ws_url TEXT, default_tier VARCHAR(20) NOT NULL, requires_approval BOOLEAN NOT NULL DEFAULT false, billing_model VARCHAR(50) NOT NULL DEFAULT 'subscription', description TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS user_product_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, product_slug VARCHAR(100) NOT NULL REFERENCES rpc_products(slug) ON DELETE CASCADE, tier VARCHAR(20) NOT NULL CHECK (tier IN ('free', 'pro', 'enterprise')), status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended', 'revoked')), monthly_quota INTEGER, requests_used INTEGER NOT NULL DEFAULT 0, requires_approval BOOLEAN NOT NULL DEFAULT false, approved_at TIMESTAMP, approved_by VARCHAR(255), notes TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(user_id, product_slug) ); CREATE TABLE IF NOT EXISTS api_key_usage_logs ( id BIGSERIAL PRIMARY KEY, api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, product_slug VARCHAR(100), method_name VARCHAR(100), request_count INTEGER NOT NULL DEFAULT 1, window_start TIMESTAMP NOT NULL DEFAULT NOW(), window_end TIMESTAMP, last_ip_address INET, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_user_product_subscriptions_user ON user_product_subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_user_product_subscriptions_product ON user_product_subscriptions(product_slug); CREATE INDEX IF NOT EXISTS idx_user_product_subscriptions_status ON user_product_subscriptions(status); CREATE INDEX IF NOT EXISTS idx_api_key_usage_logs_key ON api_key_usage_logs(api_key_id); CREATE INDEX IF NOT EXISTS idx_api_key_usage_logs_product ON api_key_usage_logs(product_slug); INSERT INTO rpc_products (slug, name, provider, vmid, http_url, ws_url, default_tier, requires_approval, billing_model, description) VALUES ('core-rpc', 'Core RPC', 'besu-core', 2101, 'https://rpc-http-prv.d-bis.org', 'wss://rpc-ws-prv.d-bis.org', 'enterprise', true, 'contract', 'Private Chain 138 Core RPC for operator-grade administration and sensitive workloads.'), ('alltra-rpc', 'Alltra RPC', 'alltra', 2102, 'http://192.168.11.212:8545', 'ws://192.168.11.212:8546', 'pro', false, 'subscription', 'Dedicated Alltra RPC lane for partner traffic, subscription access, and API-key-gated usage.'), ('thirdweb-rpc', 'Thirdweb RPC', 'thirdweb', 2103, 'http://192.168.11.217:8545', 'ws://192.168.11.217:8546', 'pro', false, 'subscription', 'Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access and API-token paywalling.') ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, provider = EXCLUDED.provider, vmid = EXCLUDED.vmid, http_url = EXCLUDED.http_url, ws_url = EXCLUDED.ws_url, default_tier = EXCLUDED.default_tier, requires_approval = EXCLUDED.requires_approval, billing_model = EXCLUDED.billing_model, description = EXCLUDED.description, updated_at = NOW();