-- ========================================== -- Chat Switchboard - PostgreSQL Schema -- ========================================== -- Enable extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "vector"; -- For embeddings (pgvector) -- ========================================== -- Core Tables -- ========================================== -- Users CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash TEXT NOT NULL, display_name VARCHAR(100), avatar_url TEXT, role VARCHAR(20) DEFAULT 'user', -- user, admin, moderator is_active BOOLEAN DEFAULT true, settings JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_login_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_email ON users(email); -- API Configurations (user's API keys) CREATE TABLE api_configs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, -- "OpenAI", "Claude", "Local Ollama" provider VARCHAR(50) NOT NULL, -- openai, anthropic, ollama, openrouter endpoint TEXT NOT NULL, api_key_encrypted TEXT, -- Encrypted at rest model_default VARCHAR(100), config JSONB DEFAULT '{}'::jsonb, -- Custom settings per provider is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_api_configs_user ON api_configs(user_id); -- ========================================== -- Feature 1: CHATS (User to LLM) -- ========================================== CREATE TABLE chats ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, model VARCHAR(100), -- Current model for this chat api_config_id UUID REFERENCES api_configs(id) ON DELETE SET NULL, system_prompt TEXT, settings JSONB DEFAULT '{}'::jsonb, -- temperature, max_tokens, etc is_archived BOOLEAN DEFAULT false, is_pinned BOOLEAN DEFAULT false, folder VARCHAR(100), -- For organization tags TEXT[], -- For filtering created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_chats_user ON chats(user_id); CREATE INDEX idx_chats_updated ON chats(updated_at DESC); CREATE INDEX idx_chats_tags ON chats USING GIN(tags); CREATE TABLE chat_messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), chat_id UUID REFERENCES chats(id) ON DELETE CASCADE, role VARCHAR(20) NOT NULL, -- user, assistant, system, tool content TEXT NOT NULL, model VARCHAR(100), -- Which model generated this (for assistant) tokens_used INTEGER, tool_calls JSONB, -- Function calls made metadata JSONB DEFAULT '{}'::jsonb, -- thinking_blocks, attachments, etc created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_chat_messages_chat ON chat_messages(chat_id, created_at); -- Model routing history (for analytics/debugging) CREATE TABLE model_routing_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), chat_id UUID REFERENCES chats(id) ON DELETE CASCADE, message_id UUID REFERENCES chat_messages(id) ON DELETE CASCADE, requested_model VARCHAR(100), routed_model VARCHAR(100), reason TEXT, -- "cost_optimization", "context_length", "manual", "fallback" latency_ms INTEGER, cost_usd NUMERIC(10, 6), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_routing_log_chat ON model_routing_log(chat_id); -- ========================================== -- Feature 2: CHANNELS (User to User + AI) -- ========================================== CREATE TABLE channels ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) UNIQUE NOT NULL, description TEXT, type VARCHAR(20) DEFAULT 'public', -- public, private, dm owner_id UUID REFERENCES users(id) ON DELETE SET NULL, settings JSONB DEFAULT '{}'::jsonb, -- ai_participants, webhooks, etc is_archived BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_channels_name ON channels(name); CREATE INDEX idx_channels_type ON channels(type); -- Channel memberships CREATE TABLE channel_members ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(20) DEFAULT 'member', -- owner, admin, member joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(channel_id, user_id) ); CREATE INDEX idx_channel_members_channel ON channel_members(channel_id); CREATE INDEX idx_channel_members_user ON channel_members(user_id); -- Channel messages CREATE TABLE channel_messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- NULL for AI messages content TEXT NOT NULL, mentions JSONB, -- {users: [uuid], models: [name]} thread_id UUID REFERENCES channel_messages(id) ON DELETE CASCADE, -- For threading attachments JSONB, -- Files, images, etc reactions JSONB DEFAULT '{}'::jsonb, -- {emoji: [user_ids]} is_ai_message BOOLEAN DEFAULT false, ai_model VARCHAR(100), -- If AI generated edited_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_channel_messages_channel ON channel_messages(channel_id, created_at DESC); CREATE INDEX idx_channel_messages_thread ON channel_messages(thread_id); CREATE INDEX idx_channel_messages_mentions ON channel_messages USING GIN(mentions); -- ========================================== -- Feature 3: NOTES & KNOWLEDGE BASES -- ========================================== CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, content TEXT NOT NULL, content_type VARCHAR(20) DEFAULT 'markdown', -- markdown, html, plain folder VARCHAR(100), tags TEXT[], is_pinned BOOLEAN DEFAULT false, is_shared BOOLEAN DEFAULT false, share_token UUID UNIQUE DEFAULT uuid_generate_v4(), parent_note_id UUID REFERENCES notes(id) ON DELETE SET NULL, -- For hierarchies created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_notes_user ON notes(user_id); CREATE INDEX idx_notes_updated ON notes(updated_at DESC); CREATE INDEX idx_notes_tags ON notes USING GIN(tags); CREATE INDEX idx_notes_share_token ON notes(share_token) WHERE is_shared = true; -- Knowledge Bases (Collections of documents) CREATE TABLE knowledge_bases ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, description TEXT, embedding_model VARCHAR(100) DEFAULT 'text-embedding-ada-002', settings JSONB DEFAULT '{}'::jsonb, -- chunk_size, overlap, etc is_public BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_knowledge_bases_user ON knowledge_bases(user_id); -- Documents in knowledge bases CREATE TABLE kb_documents ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE, filename VARCHAR(500) NOT NULL, content TEXT NOT NULL, content_type VARCHAR(50), -- application/pdf, text/markdown, etc file_size INTEGER, metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_kb_documents_kb ON kb_documents(kb_id); -- Document chunks (for RAG) CREATE TABLE kb_chunks ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), document_id UUID REFERENCES kb_documents(id) ON DELETE CASCADE, kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE, content TEXT NOT NULL, embedding vector(1536), -- For pgvector similarity search chunk_index INTEGER, metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_kb_chunks_document ON kb_chunks(document_id); CREATE INDEX idx_kb_chunks_embedding ON kb_chunks USING ivfflat (embedding vector_cosine_ops); -- ========================================== -- Feature 4: PLUGIN ORCHESTRATION -- ========================================== -- Installed extensions/plugins CREATE TABLE extensions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) UNIQUE NOT NULL, version VARCHAR(20) NOT NULL, author VARCHAR(100), description TEXT, runtime VARCHAR(20), -- python, go, node, rust entry_point TEXT NOT NULL, port INTEGER, manifest JSONB NOT NULL, -- Full extension.json is_enabled BOOLEAN DEFAULT true, is_system BOOLEAN DEFAULT false, -- Core extensions install_source VARCHAR(200), -- URL or marketplace ID installed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_extensions_enabled ON extensions(is_enabled); -- Extension tools/functions CREATE TABLE extension_tools ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), extension_id UUID REFERENCES extensions(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, description TEXT, parameters_schema JSONB NOT NULL, -- JSON Schema response_schema JSONB, is_enabled BOOLEAN DEFAULT true, UNIQUE(extension_id, name) ); CREATE INDEX idx_extension_tools_extension ON extension_tools(extension_id); -- Tool usage log (for analytics) CREATE TABLE tool_usage_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tool_id UUID REFERENCES extension_tools(id) ON DELETE SET NULL, user_id UUID REFERENCES users(id) ON DELETE SET NULL, chat_id UUID REFERENCES chats(id) ON DELETE SET NULL, channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, input_params JSONB, output_result JSONB, execution_time_ms INTEGER, success BOOLEAN, error_message TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_tool_usage_tool ON tool_usage_log(tool_id); CREATE INDEX idx_tool_usage_user ON tool_usage_log(user_id); -- ========================================== -- Feature 5: WORKFLOWS (Unique Feature!) -- ========================================== -- Workflow definitions (DAG of AI operations) CREATE TABLE workflows ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, description TEXT, graph JSONB NOT NULL, -- Node-edge DAG structure input_schema JSONB, -- Expected inputs output_schema JSONB, -- Expected outputs is_public BOOLEAN DEFAULT false, is_template BOOLEAN DEFAULT false, tags TEXT[], usage_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_workflows_user ON workflows(user_id); CREATE INDEX idx_workflows_public ON workflows(is_public) WHERE is_public = true; CREATE INDEX idx_workflows_tags ON workflows USING GIN(tags); -- Workflow executions CREATE TABLE workflow_executions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), workflow_id UUID REFERENCES workflows(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE SET NULL, input_data JSONB, output_data JSONB, status VARCHAR(20), -- running, completed, failed steps JSONB, -- Execution trace total_cost_usd NUMERIC(10, 6), total_time_ms INTEGER, error_message TEXT, started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), completed_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_workflow_executions_workflow ON workflow_executions(workflow_id); CREATE INDEX idx_workflow_executions_user ON workflow_executions(user_id); -- ========================================== -- Shared/Utility Tables -- ========================================== -- File uploads CREATE TABLE files ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, filename VARCHAR(500) NOT NULL, content_type VARCHAR(100), file_size INTEGER, storage_path TEXT NOT NULL, -- S3/local path is_public BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_files_user ON files(user_id); -- Webhooks CREATE TABLE webhooks ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(100), url TEXT NOT NULL, events TEXT[] NOT NULL, -- chat.message, channel.message, etc secret VARCHAR(100), is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_webhooks_user ON webhooks(user_id); -- Audit log CREATE TABLE audit_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE SET NULL, action VARCHAR(100) NOT NULL, resource_type VARCHAR(50), resource_id UUID, metadata JSONB, ip_address INET, user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_audit_log_user ON audit_log(user_id); CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC); -- ========================================== -- Functions & Triggers -- ========================================== -- Auto-update updated_at timestamps CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER chats_updated_at BEFORE UPDATE ON chats FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER notes_updated_at BEFORE UPDATE ON notes FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER knowledge_bases_updated_at BEFORE UPDATE ON knowledge_bases FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- Increment workflow usage count CREATE OR REPLACE FUNCTION increment_workflow_usage() RETURNS TRIGGER AS $$ BEGIN UPDATE workflows SET usage_count = usage_count + 1 WHERE id = NEW.workflow_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER workflow_executions_insert AFTER INSERT ON workflow_executions FOR EACH ROW EXECUTE FUNCTION increment_workflow_usage();