Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

20250104000000_ai_chat_system.sql22 kB
-- AI Chat System Database Migration -- CanadaGPT - Comprehensive AI chat interface with hybrid pricing -- Created: 2025-01-04 -- ============================================ -- SUBSCRIPTIONS TABLE -- ============================================ CREATE TABLE IF NOT EXISTS user_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, tier VARCHAR(20) NOT NULL CHECK (tier IN ('free', 'basic', 'pro', 'usage_only')), status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'canceled', 'suspended', 'past_due')), stripe_customer_id VARCHAR(255), stripe_subscription_id VARCHAR(255), -- Quota settings daily_quota INTEGER NOT NULL DEFAULT 0, lifetime_quota INTEGER, -- Only for free tier -- Overage settings allow_overages BOOLEAN DEFAULT TRUE, overage_limit DECIMAL(10,2) DEFAULT 10.00, current_overage_amount DECIMAL(10,2) DEFAULT 0.00, -- BYOK flags uses_byo_key BOOLEAN DEFAULT FALSE, -- Billing period current_period_start TIMESTAMP WITH TIME ZONE, current_period_end TIMESTAMP WITH TIME ZONE, -- Metadata created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), canceled_at TIMESTAMP WITH TIME ZONE, UNIQUE(user_id) ); -- Index for faster subscription lookups CREATE INDEX idx_user_subscriptions_user_id ON user_subscriptions(user_id); CREATE INDEX idx_user_subscriptions_stripe_customer ON user_subscriptions(stripe_customer_id); COMMENT ON TABLE user_subscriptions IS 'User subscription tiers and billing settings'; COMMENT ON COLUMN user_subscriptions.lifetime_quota IS 'Total queries allowed for free tier (e.g., 10)'; COMMENT ON COLUMN user_subscriptions.daily_quota IS 'Queries per day: Free=0, Basic=15, Pro=40'; COMMENT ON COLUMN user_subscriptions.uses_byo_key IS 'True if user has connected their own API key for unlimited queries'; -- ============================================ -- API KEYS TABLE (BYOK) -- ============================================ CREATE TABLE IF NOT EXISTS user_api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, provider VARCHAR(20) NOT NULL CHECK (provider IN ('anthropic', 'openai')), encrypted_key TEXT NOT NULL, encryption_iv TEXT NOT NULL, -- Initialization vector for AES-GCM encryption_tag TEXT NOT NULL, -- Auth tag for AES-GCM is_active BOOLEAN DEFAULT TRUE, last_validated_at TIMESTAMP WITH TIME ZONE, validation_error TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id, provider) ); CREATE INDEX idx_user_api_keys_user_id ON user_api_keys(user_id); CREATE INDEX idx_user_api_keys_active ON user_api_keys(user_id, is_active); COMMENT ON TABLE user_api_keys IS 'Encrypted user API keys for BYOK (Bring Your Own Key)'; COMMENT ON COLUMN user_api_keys.encrypted_key IS 'API key encrypted with AES-256-GCM'; COMMENT ON COLUMN user_api_keys.encryption_iv IS 'Initialization vector for decryption'; COMMENT ON COLUMN user_api_keys.encryption_tag IS 'Authentication tag for GCM mode'; -- ============================================ -- CONVERSATIONS TABLE -- ============================================ CREATE TABLE IF NOT EXISTS conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL DEFAULT 'New conversation', -- Context awareness context_type VARCHAR(50) CHECK (context_type IN ('general', 'mp', 'bill', 'dashboard', 'lobbying', 'spending')), context_id TEXT, -- MP ID, Bill number, etc. context_data JSONB, -- Additional context (MP name, bill title, etc.) -- Conversation state message_count INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, is_pinned BOOLEAN DEFAULT FALSE, is_archived BOOLEAN DEFAULT FALSE, -- Expiration (tier-based TTL) expires_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_conversations_user_id ON conversations(user_id); CREATE INDEX idx_conversations_updated ON conversations(user_id, updated_at DESC); CREATE INDEX idx_conversations_context ON conversations(context_type, context_id); CREATE INDEX idx_conversations_expires ON conversations(expires_at); COMMENT ON TABLE conversations IS 'Chat conversation threads with context awareness'; COMMENT ON COLUMN conversations.context_type IS 'Page context: mp, bill, dashboard, etc.'; COMMENT ON COLUMN conversations.expires_at IS 'Free=immediate, Basic=30 days, Pro=90 days'; -- ============================================ -- MESSAGES TABLE -- ============================================ CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE NOT NULL, role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'function')), content TEXT NOT NULL, -- Token tracking tokens_input INTEGER, tokens_output INTEGER, tokens_total INTEGER, -- AI provider info provider VARCHAR(20), -- 'anthropic' or 'openai' model VARCHAR(100), -- 'claude-sonnet-3-5-20241022', etc. used_byo_key BOOLEAN DEFAULT FALSE, -- Function calling function_calls JSONB, -- Array of function calls made function_results JSONB, -- Results from function calls -- Cost tracking cost_usd DECIMAL(10,6), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at); CREATE INDEX idx_messages_created ON messages(created_at DESC); COMMENT ON TABLE messages IS 'Individual messages in conversations with token and cost tracking'; COMMENT ON COLUMN messages.used_byo_key IS 'True if user API key was used (no cost to us)'; -- ============================================ -- USAGE LOGS TABLE -- ============================================ CREATE TABLE IF NOT EXISTS usage_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL, message_id UUID REFERENCES messages(id) ON DELETE SET NULL, -- Date tracking query_date DATE NOT NULL DEFAULT CURRENT_DATE, query_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Token usage tokens_input INTEGER NOT NULL, tokens_output INTEGER NOT NULL, tokens_total INTEGER NOT NULL, -- Cost tracking cost_usd DECIMAL(10,6) NOT NULL, provider VARCHAR(20) NOT NULL, model VARCHAR(100), used_byo_key BOOLEAN DEFAULT FALSE, -- Quota tracking counted_against_quota BOOLEAN DEFAULT TRUE, was_overage BOOLEAN DEFAULT FALSE, overage_charge DECIMAL(10,6) DEFAULT 0.00, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_usage_logs_user_date ON usage_logs(user_id, query_date DESC); CREATE INDEX idx_usage_logs_user_timestamp ON usage_logs(user_id, query_timestamp DESC); CREATE INDEX idx_usage_logs_billing ON usage_logs(user_id, was_overage, query_date); COMMENT ON TABLE usage_logs IS 'Detailed query usage logs for billing and analytics'; COMMENT ON COLUMN usage_logs.counted_against_quota IS 'False for BYOK queries'; COMMENT ON COLUMN usage_logs.was_overage IS 'True if query exceeded daily quota and was charged'; -- ============================================ -- CREDIT PACKS TABLE (Pay-as-you-go) -- ============================================ CREATE TABLE IF NOT EXISTS credit_packs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, -- Credits credits_purchased INTEGER NOT NULL, credits_remaining INTEGER NOT NULL, credits_used INTEGER DEFAULT 0, -- Pricing price_paid DECIMAL(10,2) NOT NULL, price_per_credit DECIMAL(10,6) NOT NULL, -- Stripe info stripe_payment_intent_id VARCHAR(255), -- Expiration (optional - credits never expire by default) expires_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), depleted_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_credit_packs_user ON credit_packs(user_id); CREATE INDEX idx_credit_packs_active ON credit_packs(user_id, depleted_at) WHERE depleted_at IS NULL; COMMENT ON TABLE credit_packs IS 'Pre-purchased query credits for usage-only tier'; COMMENT ON COLUMN credit_packs.credits_remaining IS 'Must be >= 0'; -- ============================================ -- QUOTA TRACKING VIEW -- ============================================ CREATE OR REPLACE VIEW daily_quota_usage AS SELECT ul.user_id, ul.query_date, COUNT(*) as queries_today, SUM(ul.tokens_total) as tokens_today, SUM(ul.cost_usd) as cost_today, SUM(CASE WHEN ul.was_overage THEN 1 ELSE 0 END) as overage_queries, SUM(ul.overage_charge) as overage_charges, us.tier, us.daily_quota, us.uses_byo_key FROM usage_logs ul JOIN user_subscriptions us ON ul.user_id = us.user_id WHERE ul.counted_against_quota = TRUE GROUP BY ul.user_id, ul.query_date, us.tier, us.daily_quota, us.uses_byo_key; COMMENT ON VIEW daily_quota_usage IS 'Real-time view of daily quota consumption per user'; -- ============================================ -- FUNCTIONS -- ============================================ -- Function: Check if user can make a query CREATE OR REPLACE FUNCTION can_user_query(p_user_id UUID, OUT can_query BOOLEAN, OUT reason TEXT, OUT requires_payment BOOLEAN) LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_subscription RECORD; v_queries_today INTEGER; v_lifetime_queries INTEGER; v_active_key_count INTEGER; BEGIN -- Get user subscription SELECT * INTO v_subscription FROM user_subscriptions WHERE user_id = p_user_id; -- No subscription found IF NOT FOUND THEN can_query := FALSE; reason := 'No subscription found'; requires_payment := TRUE; RETURN; END IF; -- Check subscription status IF v_subscription.status != 'active' THEN can_query := FALSE; reason := 'Subscription not active: ' || v_subscription.status; requires_payment := TRUE; RETURN; END IF; -- Check if user has BYOK enabled SELECT COUNT(*) INTO v_active_key_count FROM user_api_keys WHERE user_id = p_user_id AND is_active = TRUE; IF v_active_key_count > 0 THEN v_subscription.uses_byo_key := TRUE; END IF; -- BYOK users have unlimited queries IF v_subscription.uses_byo_key THEN can_query := TRUE; reason := 'Unlimited (BYOK)'; requires_payment := FALSE; RETURN; END IF; -- Free tier: Check lifetime quota IF v_subscription.tier = 'free' THEN SELECT COUNT(*) INTO v_lifetime_queries FROM usage_logs WHERE user_id = p_user_id AND counted_against_quota = TRUE; IF v_lifetime_queries >= COALESCE(v_subscription.lifetime_quota, 10) THEN can_query := FALSE; reason := 'Free tier quota exhausted'; requires_payment := TRUE; RETURN; END IF; can_query := TRUE; reason := 'Within free tier quota'; requires_payment := FALSE; RETURN; END IF; -- Paid tiers: Check daily quota SELECT COUNT(*) INTO v_queries_today FROM usage_logs WHERE user_id = p_user_id AND query_date = CURRENT_DATE AND counted_against_quota = TRUE; -- Within daily quota IF v_queries_today < v_subscription.daily_quota THEN can_query := TRUE; reason := 'Within daily quota'; requires_payment := FALSE; RETURN; END IF; -- Exceeded daily quota - check if overages allowed IF v_subscription.allow_overages THEN -- Check if overage limit reached IF v_subscription.current_overage_amount >= v_subscription.overage_limit THEN can_query := FALSE; reason := 'Overage limit reached'; requires_payment := FALSE; -- They've hit their limit RETURN; END IF; can_query := TRUE; reason := 'Overage allowed'; requires_payment := TRUE; -- Will be charged for overage RETURN; END IF; -- No overages allowed can_query := FALSE; reason := 'Daily quota exceeded'; requires_payment := FALSE; RETURN; END; $$; COMMENT ON FUNCTION can_user_query IS 'Check if user can make a query considering quota, BYOK, and overages'; -- Function: Track query usage CREATE OR REPLACE FUNCTION track_query_usage( p_user_id UUID, p_conversation_id UUID, p_message_id UUID, p_tokens_input INTEGER, p_tokens_output INTEGER, p_provider VARCHAR(20), p_model VARCHAR(100), p_used_byo_key BOOLEAN, p_cost_usd DECIMAL(10,6) ) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_subscription RECORD; v_queries_today INTEGER; v_is_overage BOOLEAN := FALSE; v_overage_charge DECIMAL(10,6) := 0.00; v_counted_against_quota BOOLEAN := TRUE; BEGIN -- Get subscription SELECT * INTO v_subscription FROM user_subscriptions WHERE user_id = p_user_id; -- BYOK queries don't count against quota or cost IF p_used_byo_key THEN v_counted_against_quota := FALSE; p_cost_usd := 0.00; ELSE -- Check if this is an overage query SELECT COUNT(*) INTO v_queries_today FROM usage_logs WHERE user_id = p_user_id AND query_date = CURRENT_DATE AND counted_against_quota = TRUE; -- Free tier has no overages (shouldn't reach here) IF v_subscription.tier != 'free' AND v_queries_today >= v_subscription.daily_quota THEN v_is_overage := TRUE; v_overage_charge := 0.025; -- $0.025 per query -- Update current overage amount UPDATE user_subscriptions SET current_overage_amount = current_overage_amount + v_overage_charge, updated_at = NOW() WHERE user_id = p_user_id; END IF; END IF; -- Insert usage log INSERT INTO usage_logs ( user_id, conversation_id, message_id, query_date, tokens_input, tokens_output, tokens_total, cost_usd, provider, model, used_byo_key, counted_against_quota, was_overage, overage_charge ) VALUES ( p_user_id, p_conversation_id, p_message_id, CURRENT_DATE, p_tokens_input, p_tokens_output, p_tokens_input + p_tokens_output, p_cost_usd, p_provider, p_model, p_used_byo_key, v_counted_against_quota, v_is_overage, v_overage_charge ); -- Update conversation stats UPDATE conversations SET message_count = message_count + 1, total_tokens = total_tokens + p_tokens_input + p_tokens_output, updated_at = NOW(), last_message_at = NOW() WHERE id = p_conversation_id; END; $$; COMMENT ON FUNCTION track_query_usage IS 'Record query usage and handle overage billing'; -- Function: Reset monthly overage charges CREATE OR REPLACE FUNCTION reset_monthly_overages() RETURNS INTEGER LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_reset_count INTEGER; BEGIN -- Reset overage amounts for all subscriptions UPDATE user_subscriptions SET current_overage_amount = 0.00, updated_at = NOW() WHERE current_overage_amount > 0; GET DIAGNOSTICS v_reset_count = ROW_COUNT; RETURN v_reset_count; END; $$; COMMENT ON FUNCTION reset_monthly_overages IS 'Reset monthly overage charges (run via cron on billing cycle)'; -- Function: Cleanup expired conversations CREATE OR REPLACE FUNCTION cleanup_expired_conversations() RETURNS INTEGER LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_deleted_count INTEGER; BEGIN -- Delete expired conversations and their messages (CASCADE) DELETE FROM conversations WHERE expires_at IS NOT NULL AND expires_at < NOW() AND is_pinned = FALSE; GET DIAGNOSTICS v_deleted_count = ROW_COUNT; RETURN v_deleted_count; END; $$; COMMENT ON FUNCTION cleanup_expired_conversations IS 'Delete expired conversations (run daily via cron)'; -- ============================================ -- ROW LEVEL SECURITY (RLS) -- ============================================ -- Enable RLS on all tables ALTER TABLE user_subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE user_api_keys ENABLE ROW LEVEL SECURITY; ALTER TABLE conversations ENABLE ROW LEVEL SECURITY; ALTER TABLE messages ENABLE ROW LEVEL SECURITY; ALTER TABLE usage_logs ENABLE ROW LEVEL SECURITY; ALTER TABLE credit_packs ENABLE ROW LEVEL SECURITY; -- Subscriptions: Users can only see their own CREATE POLICY user_subscriptions_select ON user_subscriptions FOR SELECT USING (auth.uid() = user_id); CREATE POLICY user_subscriptions_update ON user_subscriptions FOR UPDATE USING (auth.uid() = user_id); -- API Keys: Users can only see/manage their own CREATE POLICY user_api_keys_select ON user_api_keys FOR SELECT USING (auth.uid() = user_id); CREATE POLICY user_api_keys_insert ON user_api_keys FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY user_api_keys_update ON user_api_keys FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY user_api_keys_delete ON user_api_keys FOR DELETE USING (auth.uid() = user_id); -- Conversations: Users can only see their own CREATE POLICY conversations_select ON conversations FOR SELECT USING (auth.uid() = user_id); CREATE POLICY conversations_insert ON conversations FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY conversations_update ON conversations FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY conversations_delete ON conversations FOR DELETE USING (auth.uid() = user_id); -- Messages: Users can only see messages in their conversations CREATE POLICY messages_select ON messages FOR SELECT USING ( EXISTS ( SELECT 1 FROM conversations WHERE conversations.id = messages.conversation_id AND conversations.user_id = auth.uid() ) ); CREATE POLICY messages_insert ON messages FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM conversations WHERE conversations.id = messages.conversation_id AND conversations.user_id = auth.uid() ) ); -- Usage Logs: Users can only see their own CREATE POLICY usage_logs_select ON usage_logs FOR SELECT USING (auth.uid() = user_id); -- Credit Packs: Users can only see their own CREATE POLICY credit_packs_select ON credit_packs FOR SELECT USING (auth.uid() = user_id); -- ============================================ -- TRIGGERS -- ============================================ -- Trigger: Update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; CREATE TRIGGER user_subscriptions_updated_at BEFORE UPDATE ON user_subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER user_api_keys_updated_at BEFORE UPDATE ON user_api_keys FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER conversations_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ============================================ -- DEFAULT DATA -- ============================================ -- Insert default free subscription for new users (via trigger) CREATE OR REPLACE FUNCTION create_default_subscription() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN INSERT INTO user_subscriptions ( user_id, tier, status, daily_quota, lifetime_quota, allow_overages ) VALUES ( NEW.id, 'free', 'active', 0, -- No daily quota for free 10, -- 10 lifetime queries FALSE -- No overages on free tier ); RETURN NEW; END; $$; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION create_default_subscription(); -- ============================================ -- INDEXES FOR PERFORMANCE -- ============================================ -- Additional composite indexes CREATE INDEX idx_usage_logs_quota_check ON usage_logs(user_id, query_date, counted_against_quota); CREATE INDEX idx_messages_tokens ON messages(conversation_id, tokens_total); CREATE INDEX idx_conversations_context_lookup ON conversations(user_id, context_type, context_id); -- ============================================ -- MIGRATION COMPLETE -- ============================================ -- Verify tables created DO $$ DECLARE v_tables TEXT[] := ARRAY[ 'user_subscriptions', 'user_api_keys', 'conversations', 'messages', 'usage_logs', 'credit_packs' ]; v_table TEXT; BEGIN FOREACH v_table IN ARRAY v_tables LOOP IF NOT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = v_table ) THEN RAISE EXCEPTION 'Table % was not created', v_table; END IF; END LOOP; RAISE NOTICE 'AI Chat System migration completed successfully'; RAISE NOTICE 'Created: 6 tables, 4 functions, 1 view, RLS policies, and triggers'; END; $$;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server