Changeset 0.9.0 (#50)
This commit is contained in:
@@ -1,408 +0,0 @@
|
||||
-- ==========================================
|
||||
-- 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();
|
||||
585
server/database/migrations/001_v09_schema.sql
Normal file
585
server/database/migrations/001_v09_schema.sql
Normal file
@@ -0,0 +1,585 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — v0.9.0 Consolidated Schema
|
||||
-- ==========================================
|
||||
-- Clean-slate schema. Replaces all 001–021 migrations.
|
||||
-- Drop DB and re-create before applying.
|
||||
--
|
||||
-- Design principles:
|
||||
-- • Explicit scope enums over nullable column tri-states
|
||||
-- • Personas as trust boundaries with extensible grants
|
||||
-- • Secure by default (models hidden until admin enables)
|
||||
-- • Only tables with active handlers — no placeholder tables
|
||||
-- ==========================================
|
||||
|
||||
-- ── Extensions ──────────────────────────────
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
||||
|
||||
-- ── Utility: auto-update updated_at ─────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 1. USERS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
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'
|
||||
CHECK (role IN ('user', 'admin', 'moderator')),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
DROP TRIGGER IF EXISTS users_updated_at ON users;
|
||||
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 2. AUTH
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 3. TEAMS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = true;
|
||||
DROP TRIGGER IF EXISTS teams_updated_at ON teams;
|
||||
CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'member'
|
||||
CHECK (role IN ('admin', 'member')),
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(team_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 4. PROVIDER CONFIGS (replaces api_configs)
|
||||
-- =========================================
|
||||
-- Explicit scope enum replaces nullable column tri-state.
|
||||
-- scope='global': admin-managed, visible to all users (owner_id IS NULL)
|
||||
-- scope='team': team admin-managed (owner_id = teams.id)
|
||||
-- scope='personal': user's own keys (owner_id = users.id)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id UUID,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
api_key_enc TEXT,
|
||||
model_default VARCHAR(100),
|
||||
config JSONB DEFAULT '{}'::jsonb,
|
||||
headers JSONB DEFAULT '{}'::jsonb,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_private BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active) WHERE is_active = true;
|
||||
DROP TRIGGER IF EXISTS provider_configs_updated_at ON provider_configs;
|
||||
CREATE TRIGGER provider_configs_updated_at BEFORE UPDATE ON provider_configs
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN provider_configs.scope IS 'global=admin-managed, team=team-scoped, personal=user BYOK';
|
||||
COMMENT ON COLUMN provider_configs.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal scope';
|
||||
COMMENT ON COLUMN provider_configs.headers IS 'Custom HTTP headers (e.g. OpenRouter HTTP-Referer)';
|
||||
COMMENT ON COLUMN provider_configs.settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)';
|
||||
COMMENT ON COLUMN provider_configs.is_private IS 'Data stays on-prem (local/self-hosted provider)';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 5. MODEL CATALOG (replaces model_configs)
|
||||
-- =========================================
|
||||
-- Intrinsic capabilities for models the system knows about.
|
||||
-- Hidden by default — admin must enable.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_catalog (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
capabilities JSONB NOT NULL DEFAULT '{}',
|
||||
pricing JSONB,
|
||||
visibility VARCHAR(10) DEFAULT 'disabled'
|
||||
CHECK (visibility IN ('enabled', 'disabled', 'team')),
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(provider_config_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility) WHERE visibility = 'enabled';
|
||||
|
||||
-- Fix CHECK constraint and default for existing databases (idempotent)
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE model_catalog DROP CONSTRAINT IF EXISTS model_catalog_visibility_check;
|
||||
-- Remap old values before adding new constraint
|
||||
UPDATE model_catalog SET visibility = 'enabled' WHERE visibility = 'visible';
|
||||
UPDATE model_catalog SET visibility = 'disabled' WHERE visibility = 'hidden';
|
||||
ALTER TABLE model_catalog ADD CONSTRAINT model_catalog_visibility_check
|
||||
CHECK (visibility IN ('enabled', 'disabled', 'team'));
|
||||
ALTER TABLE model_catalog ALTER COLUMN visibility SET DEFAULT 'disabled';
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
DROP TRIGGER IF EXISTS model_catalog_updated_at ON model_catalog;
|
||||
CREATE TRIGGER model_catalog_updated_at BEFORE UPDATE ON model_catalog
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN model_catalog.visibility IS 'hidden by default — admin must enable for global models';
|
||||
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 6. PERSONAS (replaces model_presets)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS personas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
icon VARCHAR(10) DEFAULT '',
|
||||
avatar TEXT DEFAULT '',
|
||||
|
||||
-- Base model binding
|
||||
base_model_id TEXT NOT NULL,
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
|
||||
-- Behavioral configuration
|
||||
system_prompt TEXT DEFAULT '',
|
||||
temperature FLOAT,
|
||||
max_tokens INT,
|
||||
thinking_budget INT,
|
||||
top_p FLOAT,
|
||||
|
||||
-- Scope & ownership
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id UUID,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
|
||||
-- State
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_shared BOOLEAN DEFAULT false,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active) WHERE is_active = true;
|
||||
DROP TRIGGER IF EXISTS personas_updated_at ON personas;
|
||||
CREATE TRIGGER personas_updated_at BEFORE UPDATE ON personas
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN personas.scope IS 'global=all users, team=team members, personal=creator only';
|
||||
COMMENT ON COLUMN personas.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal';
|
||||
COMMENT ON COLUMN personas.is_shared IS 'Personal Personas shared with others (read-only)';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 7. PERSONA GRANTS (extensible resource binding)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
grant_type VARCHAR(30) NOT NULL,
|
||||
grant_ref TEXT NOT NULL,
|
||||
config JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(persona_id, grant_type, grant_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type);
|
||||
|
||||
COMMENT ON COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base (future), api_endpoint (future)';
|
||||
COMMENT ON COLUMN persona_grants.grant_ref IS 'tool: function name. knowledge_base: UUID. api_endpoint: identifier.';
|
||||
COMMENT ON COLUMN persona_grants.config IS 'Type-specific config, e.g. {"read_only": true} for KB grants';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 8. PLATFORM POLICIES (replaces scattered global_settings checks)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_policies (
|
||||
key VARCHAR(50) PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_by UUID REFERENCES users(id),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Secure by default
|
||||
INSERT INTO platform_policies (key, value) VALUES
|
||||
('allow_user_byok', 'false'),
|
||||
('allow_user_personas', 'false'),
|
||||
('allow_raw_model_access', 'true'),
|
||||
('allow_registration', 'true'),
|
||||
('default_user_active', 'false'),
|
||||
('allow_team_providers', 'true')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 9. GLOBAL SETTINGS (non-policy config)
|
||||
-- =========================================
|
||||
-- Retained for banner config, site branding, etc.
|
||||
-- Policy-like keys move to platform_policies.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Seed defaults
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('registration', '{"enabled": true}'::jsonb),
|
||||
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb),
|
||||
('banner', '{
|
||||
"enabled": false,
|
||||
"text": "",
|
||||
"position": "both",
|
||||
"bg": "#007a33",
|
||||
"fg": "#ffffff"
|
||||
}'::jsonb),
|
||||
('banner_presets', '{
|
||||
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
|
||||
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
|
||||
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
|
||||
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
|
||||
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
|
||||
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
|
||||
}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 10. USER MODEL SETTINGS (replaces user_model_preferences)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_model_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
hidden BOOLEAN DEFAULT false,
|
||||
preferred_temperature FLOAT,
|
||||
preferred_max_tokens INT,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id);
|
||||
DROP TRIGGER IF EXISTS user_model_settings_updated_at ON user_model_settings;
|
||||
CREATE TRIGGER user_model_settings_updated_at BEFORE UPDATE ON user_model_settings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 11. CHANNELS (unified chats)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
type VARCHAR(20) DEFAULT 'direct'
|
||||
CHECK (type IN ('direct', 'group', 'channel')),
|
||||
model VARCHAR(100),
|
||||
system_prompt TEXT,
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
is_archived BOOLEAN DEFAULT false,
|
||||
is_pinned BOOLEAN DEFAULT false,
|
||||
folder_id UUID, -- FK added after folders table
|
||||
folder TEXT, -- backward compat: simple text folder name
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
tags TEXT[],
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_tags ON channels USING GIN(tags);
|
||||
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
|
||||
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN channels.type IS 'direct=1:1 AI chat, group=multi-model, channel=named persistent';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 12. MESSAGES (with tree/forking support)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL
|
||||
CHECK (role IN ('user', 'assistant', 'system', 'tool')),
|
||||
content TEXT NOT NULL,
|
||||
model VARCHAR(100),
|
||||
tokens_used INTEGER,
|
||||
tool_calls JSONB,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
parent_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
||||
sibling_index INTEGER DEFAULT 0,
|
||||
participant_type VARCHAR(10) DEFAULT 'user',
|
||||
participant_id VARCHAR(255),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_alive ON messages(channel_id, created_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent_alive ON messages(parent_id, sibling_index)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON COLUMN messages.parent_id IS 'Tree parent for conversation forking';
|
||||
COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)';
|
||||
COMMENT ON COLUMN messages.participant_type IS 'user or model';
|
||||
COMMENT ON COLUMN messages.participant_id IS 'user UUID or model identifier string';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 13. CHANNEL MEMBERS & MODELS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) DEFAULT 'member',
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_read_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_members_channel ON channel_members(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_members_user ON channel_members(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_models (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
model_id VARCHAR(255) NOT NULL,
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
display_name VARCHAR(100),
|
||||
system_prompt TEXT,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 14. CHANNEL CURSORS (forking navigation)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_cursors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
active_leaf_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 15. FOLDERS & PROJECTS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, name, parent_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id);
|
||||
DROP TRIGGER IF EXISTS folders_updated_at ON folders;
|
||||
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Now add the FK from channels
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE channels ADD CONSTRAINT fk_channels_folder
|
||||
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_user ON projects(user_id);
|
||||
DROP TRIGGER IF EXISTS projects_updated_at ON projects;
|
||||
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_channels (
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 16. NOTES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content TEXT DEFAULT '',
|
||||
folder_path TEXT DEFAULT '/',
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
metadata JSONB DEFAULT '{}',
|
||||
source_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
search_vector TSVECTOR,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_search ON notes USING GIN(search_vector);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notes_search_update_fn()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B');
|
||||
NEW.updated_at := NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS notes_search_update ON notes;
|
||||
CREATE TRIGGER notes_search_update
|
||||
BEFORE INSERT OR UPDATE OF title, content ON notes
|
||||
FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 17. AUDIT LOG
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id VARCHAR(255),
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
|
||||
COMMENT ON TABLE audit_log IS 'Immutable audit trail of all mutating operations';
|
||||
@@ -1,20 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard - Refresh Tokens
|
||||
-- ==========================================
|
||||
-- Supports JWT refresh token rotation.
|
||||
-- Old tokens are revoked on each refresh.
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
revoked_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL;
|
||||
|
||||
-- Cleanup: auto-delete expired tokens older than 30 days
|
||||
-- (run periodically or via pg_cron if available)
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Migration 003: Global Settings
|
||||
-- Stores application-wide configuration managed by admins.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Seed defaults
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('registration', '{"enabled": true}'::jsonb),
|
||||
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
@@ -1,12 +0,0 @@
|
||||
-- Model configurations: admin-curated list of available models
|
||||
CREATE TABLE IF NOT EXISTS model_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
api_config_id UUID REFERENCES api_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
is_enabled BOOLEAN DEFAULT true,
|
||||
capabilities JSONB DEFAULT '{"tool": false, "thinking": false, "vision": false, "code": false}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(api_config_id, model_id)
|
||||
);
|
||||
@@ -1,60 +0,0 @@
|
||||
-- Migration 005: Provider Capabilities & Custom Headers
|
||||
--
|
||||
-- Adds provider-level configuration (custom headers, provider-specific settings)
|
||||
-- and expands model capabilities to include output token limits.
|
||||
-- This eliminates hardcoded max_tokens defaults throughout the system.
|
||||
|
||||
-- ── api_configs: custom headers and global flag ──
|
||||
ALTER TABLE api_configs
|
||||
ADD COLUMN IF NOT EXISTS custom_headers JSONB DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS provider_settings JSONB DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS is_global BOOLEAN DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN api_configs.custom_headers IS 'Extra HTTP headers sent with every request (e.g. OpenRouter HTTP-Referer)';
|
||||
COMMENT ON COLUMN api_configs.provider_settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)';
|
||||
COMMENT ON COLUMN api_configs.is_global IS 'Admin-managed configs visible to all users';
|
||||
|
||||
-- Backfill: existing admin-created configs (user_id IS NULL) are global
|
||||
UPDATE api_configs SET is_global = true WHERE user_id IS NULL;
|
||||
|
||||
-- ── model_configs: richer capabilities ──
|
||||
-- The existing capabilities JSONB gets richer fields.
|
||||
-- No schema change needed (it's JSONB), but let's update existing rows
|
||||
-- that have the old minimal shape to include the new fields.
|
||||
-- New canonical shape:
|
||||
-- {
|
||||
-- "streaming": true,
|
||||
-- "tool_calling": false,
|
||||
-- "vision": false,
|
||||
-- "thinking": false,
|
||||
-- "reasoning": false,
|
||||
-- "code_optimized": false,
|
||||
-- "max_context": 0,
|
||||
-- "max_output_tokens": 0,
|
||||
-- "web_search": false
|
||||
-- }
|
||||
-- max_output_tokens = 0 means "not set, use provider/heuristic default"
|
||||
|
||||
-- Migrate old "tool" key to "tool_calling" for consistency
|
||||
UPDATE model_configs
|
||||
SET capabilities = capabilities - 'tool' || jsonb_build_object('tool_calling', COALESCE(capabilities->>'tool', 'false')::boolean)
|
||||
WHERE capabilities ? 'tool' AND NOT capabilities ? 'tool_calling';
|
||||
|
||||
-- Migrate old "code" key to "code_optimized"
|
||||
UPDATE model_configs
|
||||
SET capabilities = capabilities - 'code' || jsonb_build_object('code_optimized', COALESCE(capabilities->>'code', 'false')::boolean)
|
||||
WHERE capabilities ? 'code' AND NOT capabilities ? 'code_optimized';
|
||||
|
||||
-- ── user_model_preferences ──
|
||||
-- Users can enable/disable models from global providers for personal use.
|
||||
CREATE TABLE IF NOT EXISTS user_model_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
model_config_id UUID NOT NULL REFERENCES model_configs(id) ON DELETE CASCADE,
|
||||
is_enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, model_config_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_model_prefs_user ON user_model_preferences(user_id);
|
||||
@@ -1,114 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 006: Unify Chats → Channels
|
||||
-- ==========================================
|
||||
-- "Everything is a channel." Merges the separate chats/channels
|
||||
-- schema into a single unified channel model.
|
||||
--
|
||||
-- The old channels/channel_members/channel_messages tables (from 001)
|
||||
-- were never populated — safe to drop and rebuild on top of chats.
|
||||
-- ==========================================
|
||||
|
||||
-- ── 1. Drop unused legacy channel tables ────
|
||||
-- These were placeholders from 001; no data, no handlers.
|
||||
-- Order matters: drop dependents first.
|
||||
|
||||
-- Remove FK references in tool_usage_log before dropping
|
||||
ALTER TABLE tool_usage_log DROP CONSTRAINT IF EXISTS tool_usage_log_channel_id_fkey;
|
||||
ALTER TABLE tool_usage_log DROP COLUMN IF EXISTS channel_id;
|
||||
|
||||
DROP TABLE IF EXISTS channel_messages CASCADE;
|
||||
DROP TABLE IF EXISTS channel_members CASCADE;
|
||||
DROP TABLE IF EXISTS channels CASCADE;
|
||||
|
||||
-- Drop the old trigger (will re-create after rename)
|
||||
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
|
||||
|
||||
-- ── 2. Rename chats → channels ──────────────
|
||||
|
||||
ALTER TABLE chats RENAME TO channels;
|
||||
ALTER TABLE chat_messages RENAME TO messages;
|
||||
|
||||
-- Rename columns to match new schema
|
||||
ALTER TABLE messages RENAME COLUMN chat_id TO channel_id;
|
||||
|
||||
-- Rename indexes
|
||||
ALTER INDEX IF EXISTS idx_chats_user RENAME TO idx_channels_user;
|
||||
ALTER INDEX IF EXISTS idx_chats_updated RENAME TO idx_channels_updated;
|
||||
ALTER INDEX IF EXISTS idx_chats_tags RENAME TO idx_channels_tags;
|
||||
ALTER INDEX IF EXISTS idx_chat_messages_chat RENAME TO idx_messages_channel;
|
||||
|
||||
-- Rename constraints (PK and FK auto-renamed with table on some PG versions,
|
||||
-- but let's be explicit for the FK)
|
||||
-- Note: PG auto-renames PKs but not FKs or check constraints
|
||||
|
||||
-- Rename the updated_at trigger
|
||||
DROP TRIGGER IF EXISTS chats_updated_at ON channels;
|
||||
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ── 3. Add channel type + description ───────
|
||||
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'direct';
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
|
||||
COMMENT ON COLUMN channels.type IS 'direct=1:1 AI chat, group=multi-model, channel=named persistent';
|
||||
|
||||
-- Backfill: all existing rows are 1:1 AI chats
|
||||
UPDATE channels SET type = 'direct' WHERE type IS NULL;
|
||||
|
||||
-- Index on type for filtered queries
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
|
||||
|
||||
-- ── 4. Add message tree (parent_id) ─────────
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES messages(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
|
||||
|
||||
-- Backfill linear parent chains on existing messages.
|
||||
-- Each message's parent is the previous message in the same channel (by created_at).
|
||||
WITH ordered AS (
|
||||
SELECT id, channel_id, created_at,
|
||||
LAG(id) OVER (PARTITION BY channel_id ORDER BY created_at) AS prev_id
|
||||
FROM messages
|
||||
)
|
||||
UPDATE messages m
|
||||
SET parent_id = o.prev_id
|
||||
FROM ordered o
|
||||
WHERE m.id = o.id AND o.prev_id IS NOT NULL AND m.parent_id IS NULL;
|
||||
|
||||
-- ── 5. Add participant columns on messages ──
|
||||
-- Decouples messages from user-only: AI models are participants too.
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_type VARCHAR(10) DEFAULT 'user';
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_id VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN messages.participant_type IS 'user or model';
|
||||
COMMENT ON COLUMN messages.participant_id IS 'user UUID or model identifier string';
|
||||
|
||||
-- Backfill: user messages get the channel owner's user_id;
|
||||
-- assistant messages get the model name as participant_id.
|
||||
UPDATE messages m
|
||||
SET participant_type = CASE WHEN m.role = 'assistant' THEN 'model' ELSE 'user' END,
|
||||
participant_id = CASE
|
||||
WHEN m.role = 'assistant' THEN COALESCE(m.model, 'unknown')
|
||||
ELSE (SELECT c.user_id::text FROM channels c WHERE c.id = m.channel_id)
|
||||
END
|
||||
WHERE m.participant_id IS NULL;
|
||||
|
||||
-- ── 6. Update model_routing_log FK ──────────
|
||||
|
||||
-- The FK column was named chat_id — rename it
|
||||
ALTER TABLE model_routing_log RENAME COLUMN chat_id TO channel_id;
|
||||
ALTER INDEX IF EXISTS idx_routing_log_chat RENAME TO idx_routing_log_channel;
|
||||
|
||||
-- message_id FK still valid (messages table was renamed, FK follows)
|
||||
|
||||
-- ── 7. Update tool_usage_log ────────────────
|
||||
-- We already dropped the old channel_id column above.
|
||||
-- Re-add it pointing to the unified channels table.
|
||||
-- Also rename the old chat_id column.
|
||||
|
||||
ALTER TABLE tool_usage_log RENAME COLUMN chat_id TO channel_id;
|
||||
-- The FK auto-follows the table rename, but let's be safe:
|
||||
-- (chat_id FK pointed to chats(id), which is now channels(id) — PG handles this)
|
||||
@@ -1,56 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 007: Channel Members & Models
|
||||
-- ==========================================
|
||||
-- Membership and model assignment tables for
|
||||
-- multi-user and multi-model channels.
|
||||
-- ==========================================
|
||||
|
||||
-- ── Channel Members ─────────────────────────
|
||||
-- Who is in this channel (human participants).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) DEFAULT 'member', -- owner, admin, member
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_read_at TIMESTAMPTZ 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);
|
||||
|
||||
-- Backfill: existing channels are 1:1, so the channel owner is the sole member.
|
||||
INSERT INTO channel_members (channel_id, user_id, role)
|
||||
SELECT id, user_id, 'owner'
|
||||
FROM channels
|
||||
WHERE user_id IS NOT NULL
|
||||
ON CONFLICT (channel_id, user_id) DO NOTHING;
|
||||
|
||||
-- ── Channel Models ──────────────────────────
|
||||
-- Which AI models are assigned to this channel.
|
||||
-- For direct chats this is one model; for group/channel
|
||||
-- there can be multiple with @mention routing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_models (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
model_id VARCHAR(255) NOT NULL, -- e.g. 'claude-sonnet-4'
|
||||
api_config_id UUID REFERENCES api_configs(id) ON DELETE SET NULL,
|
||||
display_name VARCHAR(100), -- optional alias in this channel
|
||||
system_prompt TEXT, -- per-model system prompt override
|
||||
settings JSONB DEFAULT '{}'::jsonb, -- temperature, max_tokens overrides
|
||||
is_default BOOLEAN DEFAULT false, -- auto-complete (no @mention needed)
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_channel_models_channel ON channel_models(channel_id);
|
||||
|
||||
-- Backfill: existing channels have a model in the channels.model column.
|
||||
INSERT INTO channel_models (channel_id, model_id, api_config_id, is_default)
|
||||
SELECT id, model, api_config_id, true
|
||||
FROM channels
|
||||
WHERE model IS NOT NULL AND model != ''
|
||||
ON CONFLICT (channel_id, model_id) DO NOTHING;
|
||||
@@ -1,29 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 008: Channel Cursors
|
||||
-- ==========================================
|
||||
-- Tracks each user's active branch position
|
||||
-- per channel. Essential for conversation forking.
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_cursors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
active_leaf_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_channel_cursors_channel ON channel_cursors(channel_id);
|
||||
CREATE INDEX idx_channel_cursors_user ON channel_cursors(user_id);
|
||||
|
||||
-- Backfill: set cursor to the last message in each channel for the owner.
|
||||
INSERT INTO channel_cursors (channel_id, user_id, active_leaf_id)
|
||||
SELECT c.id, c.user_id, (
|
||||
SELECT m.id FROM messages m
|
||||
WHERE m.channel_id = c.id
|
||||
ORDER BY m.created_at DESC LIMIT 1
|
||||
)
|
||||
FROM channels c
|
||||
WHERE c.user_id IS NOT NULL
|
||||
ON CONFLICT (channel_id, user_id) DO NOTHING;
|
||||
@@ -1,57 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 009: Folders & Projects
|
||||
-- ==========================================
|
||||
-- Organizational structures for channels.
|
||||
-- Folders are simple containers; projects are
|
||||
-- tagged collections that can span folders.
|
||||
-- ==========================================
|
||||
|
||||
-- ── Folders ─────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, name, parent_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_folders_user ON folders(user_id);
|
||||
CREATE INDEX idx_folders_parent ON folders(parent_id);
|
||||
|
||||
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Add folder_id to channels (replaces the old text folder column)
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES folders(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id);
|
||||
|
||||
-- ── Projects ────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7), -- hex color for UI badge
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_projects_user ON projects(user_id);
|
||||
|
||||
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Junction: channels can belong to multiple projects
|
||||
CREATE TABLE IF NOT EXISTS project_channels (
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_project_channels_channel ON project_channels(channel_id);
|
||||
@@ -1,32 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 010: Environment Banners
|
||||
-- ==========================================
|
||||
-- Environment banners for deployment context
|
||||
-- (dev, staging, production, etc). Stored in
|
||||
-- global_settings with a dedicated key.
|
||||
-- ==========================================
|
||||
|
||||
-- Seed default banner config (disabled).
|
||||
-- Schema: { enabled, text, position, bg, fg }
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('banner', '{
|
||||
"enabled": false,
|
||||
"text": "",
|
||||
"position": "both",
|
||||
"bg": "#007a33",
|
||||
"fg": "#ffffff"
|
||||
}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Banner presets for quick selection in admin UI.
|
||||
-- Generic environment labels — admins can set custom text.
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('banner_presets', '{
|
||||
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
|
||||
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
|
||||
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
|
||||
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
|
||||
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
|
||||
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
|
||||
}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 011: Replace banner presets
|
||||
-- ==========================================
|
||||
-- Replaces legacy presets with
|
||||
-- generic environment labels. Existing databases
|
||||
-- that ran 010 have the old presets; this overwrites.
|
||||
-- ==========================================
|
||||
|
||||
UPDATE global_settings
|
||||
SET value = '{
|
||||
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
|
||||
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
|
||||
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
|
||||
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
|
||||
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
|
||||
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
|
||||
}'::jsonb
|
||||
WHERE key = 'banner_presets';
|
||||
@@ -1,33 +0,0 @@
|
||||
-- Model Presets: named wrappers around base models with bundled config.
|
||||
-- Admins create org-wide presets, users create personal ones.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_presets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
base_model_id TEXT NOT NULL, -- e.g. "gpt-4o", "claude-sonnet-4-20250514"
|
||||
api_config_id UUID REFERENCES api_configs(id) ON DELETE CASCADE,
|
||||
system_prompt TEXT DEFAULT '',
|
||||
temperature REAL, -- NULL = use model default
|
||||
max_tokens INTEGER, -- NULL = use model default
|
||||
tools_enabled JSONB DEFAULT '[]'::jsonb, -- reserved for future tool framework
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id UUID, -- nullable; for future team scoping
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
is_shared BOOLEAN DEFAULT false, -- personal presets visible to others
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
icon VARCHAR(10) DEFAULT '', -- emoji or short icon code
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_presets_scope ON model_presets(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_presets_created_by ON model_presets(created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_presets_team ON model_presets(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE model_presets IS 'Named model configurations: org-wide or personal wrappers around base models';
|
||||
COMMENT ON COLUMN model_presets.base_model_id IS 'The underlying model_id (matches model_configs.model_id)';
|
||||
COMMENT ON COLUMN model_presets.api_config_id IS 'Which provider config to use (NULL = resolve at completion time)';
|
||||
COMMENT ON COLUMN model_presets.scope IS 'global = admin-created for all users; team = team-scoped; personal = user-created';
|
||||
COMMENT ON COLUMN model_presets.tools_enabled IS 'JSON array of tool names enabled for this preset (future use)';
|
||||
@@ -1,28 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 013: Message Forking Support
|
||||
-- ==========================================
|
||||
-- Adds soft delete and sibling ordering to support
|
||||
-- conversation forking (edit, regenerate, branch).
|
||||
-- See ARCHITECTURE.md §8 for the full tree model.
|
||||
-- ==========================================
|
||||
|
||||
-- Soft delete: pruned branches keep their structure for undo
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
-- Sibling ordering: explicit position among children of same parent
|
||||
-- First child = 0, second = 1, etc. Set at insert time.
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS sibling_index INTEGER DEFAULT 0;
|
||||
|
||||
-- Partial index: fast lookup of live messages only
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_alive
|
||||
ON messages(channel_id, created_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- Children of a parent (for sibling queries)
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent_alive
|
||||
ON messages(parent_id, sibling_index)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- Backfill sibling_index for existing messages.
|
||||
-- In linear conversations every message is the sole child of its parent,
|
||||
-- so all get sibling_index = 0 (already the default). No update needed.
|
||||
@@ -1,50 +0,0 @@
|
||||
-- 014_notes.sql — Notes table with full-text search
|
||||
--
|
||||
-- Notes are user-scoped persistent documents. The LLM can create, search,
|
||||
-- and update them via built-in tools (note_create, note_search, etc.).
|
||||
-- Full-text search uses PostgreSQL tsvector — zero additional infrastructure.
|
||||
--
|
||||
-- DROP first: if a previous deployment created an incomplete version of
|
||||
-- this table (e.g. missing search_vector), IF NOT EXISTS would skip and
|
||||
-- the indexes/trigger would fail. Safe because 014 was never recorded
|
||||
-- in schema_migrations — any existing data is from a failed attempt.
|
||||
|
||||
DROP TABLE IF EXISTS notes CASCADE;
|
||||
|
||||
CREATE TABLE notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content TEXT DEFAULT '',
|
||||
folder_path TEXT DEFAULT '/',
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
metadata JSONB DEFAULT '{}',
|
||||
source_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
search_vector TSVECTOR,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notes_user ON notes(user_id);
|
||||
CREATE INDEX idx_notes_search ON notes USING GIN(search_vector);
|
||||
CREATE INDEX idx_notes_folder ON notes(user_id, folder_path);
|
||||
CREATE INDEX idx_notes_tags ON notes USING GIN(tags);
|
||||
CREATE INDEX idx_notes_updated ON notes(user_id, updated_at DESC);
|
||||
|
||||
-- Auto-update search vector from title + content
|
||||
CREATE OR REPLACE FUNCTION notes_search_update_fn()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B');
|
||||
NEW.updated_at := NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Drop first to allow re-run
|
||||
DROP TRIGGER IF EXISTS notes_search_update ON notes;
|
||||
CREATE TRIGGER notes_search_update
|
||||
BEFORE INSERT OR UPDATE OF title, content ON notes
|
||||
FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn();
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Avatar support for model presets.
|
||||
-- Users table already has avatar_url from 001_full_schema.sql.
|
||||
|
||||
ALTER TABLE model_presets ADD COLUMN IF NOT EXISTS avatar TEXT DEFAULT '';
|
||||
|
||||
COMMENT ON COLUMN model_presets.avatar IS 'Base64 data URI of preset avatar image (128x128 PNG), empty = use icon emoji';
|
||||
@@ -1,76 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 016: Teams
|
||||
-- ==========================================
|
||||
-- Teams are the middle tier between system admin and individual users.
|
||||
-- A team admin can manage members, create team-scoped presets,
|
||||
-- and enforce provider policies — without system-wide access.
|
||||
-- ==========================================
|
||||
|
||||
-- ── 1. Teams table ──────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
settings JSONB DEFAULT '{}'::jsonb, -- team-level policies
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = true;
|
||||
|
||||
CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE teams IS 'Organizational teams with scoped administration';
|
||||
COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}';
|
||||
|
||||
-- ── 2. Team Members table ───────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'member'
|
||||
CHECK (role IN ('admin', 'member')),
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(team_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
||||
|
||||
COMMENT ON TABLE team_members IS 'Team membership with per-team roles (admin or member)';
|
||||
COMMENT ON COLUMN team_members.role IS 'admin = manages team; member = uses team resources';
|
||||
|
||||
-- ── 3. Wire model_presets.team_id FK ────────
|
||||
-- Column already exists (migration 012), just add the FK constraint.
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE model_presets
|
||||
ADD CONSTRAINT fk_presets_team
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ── 4. Add team_id to channels ──────────────
|
||||
-- Team channels are visible to all team members.
|
||||
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
-- ── 5. Private provider flag ────────────────
|
||||
-- Marks a provider as local/self-hosted (data stays on-prem).
|
||||
|
||||
ALTER TABLE api_configs ADD COLUMN IF NOT EXISTS is_private BOOLEAN DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN api_configs.is_private IS 'Private/self-hosted provider — data does not leave network';
|
||||
|
||||
-- ── 6. Add team_id to notes ─────────────────
|
||||
-- Team notes are shared within the team.
|
||||
|
||||
ALTER TABLE notes ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;
|
||||
@@ -1,37 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 017: Audit Log
|
||||
-- ==========================================
|
||||
-- Immutable append-only log of all mutating actions.
|
||||
-- Required for enterprise compliance (SOC2, FedRAMP, HIPAA).
|
||||
-- ==========================================
|
||||
|
||||
-- Drop stale table if left from a prior partial run
|
||||
DROP TABLE IF EXISTS audit_log CASCADE;
|
||||
|
||||
CREATE TABLE audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL, -- e.g. 'user.create', 'team.add_member'
|
||||
resource_type VARCHAR(50) NOT NULL, -- e.g. 'user', 'team', 'preset', 'channel'
|
||||
resource_id VARCHAR(255), -- UUID or identifier of affected resource
|
||||
metadata JSONB DEFAULT '{}'::jsonb, -- action-specific details
|
||||
ip_address VARCHAR(45), -- IPv4 or IPv6
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Time-range queries (admin viewer, compliance exports)
|
||||
CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC);
|
||||
|
||||
-- Filter by actor
|
||||
CREATE INDEX idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL;
|
||||
|
||||
-- Filter by resource
|
||||
CREATE INDEX idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
|
||||
-- Filter by action
|
||||
CREATE INDEX idx_audit_log_action ON audit_log(action);
|
||||
|
||||
COMMENT ON TABLE audit_log IS 'Immutable audit trail of all mutating operations';
|
||||
COMMENT ON COLUMN audit_log.action IS 'Dotted action name: resource.verb (e.g. user.create, team.add_member)';
|
||||
COMMENT ON COLUMN audit_log.metadata IS 'Action-specific context: old/new values, affected fields, etc.';
|
||||
@@ -1,36 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 018: Model Visibility
|
||||
-- ==========================================
|
||||
-- Replace binary is_enabled with three-state visibility:
|
||||
-- 'enabled' — visible to all users in model selector
|
||||
-- 'disabled' — hidden from everyone
|
||||
-- 'team' — only available to team admins for building presets
|
||||
-- ==========================================
|
||||
|
||||
-- Add new column (idempotent)
|
||||
ALTER TABLE model_configs ADD COLUMN IF NOT EXISTS visibility VARCHAR(10) DEFAULT 'disabled';
|
||||
|
||||
-- Backfill from is_enabled if it still exists
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'model_configs' AND column_name = 'is_enabled'
|
||||
) THEN
|
||||
UPDATE model_configs SET visibility = CASE
|
||||
WHEN is_enabled = true THEN 'enabled'
|
||||
ELSE 'disabled'
|
||||
END;
|
||||
ALTER TABLE model_configs DROP COLUMN is_enabled;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Ensure NOT NULL (safe: DEFAULT already covers new rows)
|
||||
UPDATE model_configs SET visibility = 'disabled' WHERE visibility IS NULL;
|
||||
ALTER TABLE model_configs ALTER COLUMN visibility SET NOT NULL;
|
||||
|
||||
-- Constraint (drop first for idempotency)
|
||||
ALTER TABLE model_configs DROP CONSTRAINT IF EXISTS chk_model_visibility;
|
||||
ALTER TABLE model_configs ADD CONSTRAINT chk_model_visibility
|
||||
CHECK (visibility IN ('enabled', 'disabled', 'team'));
|
||||
|
||||
COMMENT ON COLUMN model_configs.visibility IS 'enabled=all users, team=team admin presets only, disabled=hidden';
|
||||
@@ -1,8 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 019: (superseded by 020)
|
||||
-- ==========================================
|
||||
-- Original CREATE TABLE IF NOT EXISTS was a no-op because
|
||||
-- user_model_preferences already existed from migration 005.
|
||||
-- The actual rework is in 020_user_model_preferences_rework.sql.
|
||||
-- ==========================================
|
||||
SELECT 1;
|
||||
@@ -1,24 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 019: User Model Preferences (rework)
|
||||
-- ==========================================
|
||||
-- The user_model_preferences table was created in 005 with:
|
||||
-- id UUID PK, user_id UUID, model_config_id UUID FK, is_enabled BOOL
|
||||
-- That schema ties preferences to model_configs rows (global only).
|
||||
-- We need string-based model_id to support personal provider models too,
|
||||
-- plus a 'hidden' column with clearer semantics.
|
||||
--
|
||||
-- Strategy: add new columns, add unique constraint for UPSERT.
|
||||
-- Old columns (model_config_id, is_enabled) remain for backward compat.
|
||||
-- ==========================================
|
||||
|
||||
-- Add new columns if they don't exist
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS model_id VARCHAR(255);
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS hidden BOOLEAN DEFAULT false;
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
-- Make model_config_id nullable (new rows use model_id instead)
|
||||
ALTER TABLE user_model_preferences ALTER COLUMN model_config_id DROP NOT NULL;
|
||||
|
||||
-- Unique constraint for UPSERT — NULLs are distinct in PG so old rows won't conflict
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_model_pref_user_model
|
||||
ON user_model_preferences (user_id, model_id);
|
||||
@@ -1,16 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Migration 021: Team Providers
|
||||
-- ==========================================
|
||||
-- Adds team_id to api_configs, enabling teams to have their own
|
||||
-- provider configs managed by team admins.
|
||||
--
|
||||
-- Provider hierarchy: global (user_id IS NULL, is_global=true)
|
||||
-- → team (team_id IS NOT NULL)
|
||||
-- → personal (user_id IS NOT NULL)
|
||||
-- ==========================================
|
||||
|
||||
-- Add team_id FK to api_configs
|
||||
ALTER TABLE api_configs ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id) ON DELETE CASCADE;
|
||||
CREATE INDEX IF NOT EXISTS idx_api_configs_team ON api_configs(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN api_configs.team_id IS 'Team-scoped provider — managed by team admins, visible to team members';
|
||||
Reference in New Issue
Block a user