step 4: fresh kernel-only migrations

Deleted all 23+23 old chat-switchboard migration files.
Wrote 9+9 clean kernel-only migrations in postgres/ and sqlite/ subdirs.

27 tables per dialect covering all 20 store interfaces:
  001: users, refresh_tokens, policies, settings, presence, oidc
  002: teams, team_members, groups, group_members
  003: packages, package_user_settings, ext_permissions, ext_data_tables
  004: ext_connections, ext_dependencies, resource_grants
  005: notifications, notification_preferences
  006: audit_log
  007: workflows, workflow_stages, workflow_versions
  008: tasks, task_runs
  009: ws_tickets, rate_limit_counters

Dropped: providers, personas, channels, messages, knowledge, notes,
memory, workspaces, projects, folders, files, usage_log, tool_health,
workflow_assignments, ext_view_channels.

Schema changes vs old:
  - pgvector extension removed (no KB embeddings)
  - resource_grants: resource_type CHECK removed (open-ended)
  - workflow_stages: persona_id kept as nullable TEXT (no FK)
  - workflow_stages: stage_mode default=form_only, added custom
  - tasks: dropped output_channel_id, provider_config_id columns
  - tasks: task_type removed prompt, output_mode: notification|webhook|log
  - task_runs: dropped channel_id
  - platform_policies: kernel-only seeds
  - global_settings: kernel-only seeds, site name=Switchboard Core
  - Everyone group: kernel permissions (extension.use, workflow.submit)

-2815/+487 lines.
This commit is contained in:
2026-03-25 20:22:02 -04:00
parent c3e9dcf731
commit ebea16344c
54 changed files with 487 additions and 2815 deletions

View File

@@ -1,183 +0,0 @@
-- ==========================================
-- Chat Switchboard — 003 Providers & Routing
-- ==========================================
-- Provider configs, model catalog, pricing, health, capability
-- overrides, and routing policies.
-- Consolidated v0.28.0: adds capability_match policy type.
-- ==========================================
-- =========================================
-- PROVIDER CONFIGS
-- =========================================
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,
-- Encrypted API key (AES-256-GCM)
api_key_enc BYTEA,
key_nonce BYTEA,
key_scope TEXT NOT NULL DEFAULT 'global',
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,
proxy_mode TEXT NOT NULL DEFAULT 'system'
CHECK (proxy_mode IN ('system', 'direct', 'custom')),
proxy_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_proxy_url CHECK (proxy_mode != 'custom' OR proxy_url IS NOT NULL)
);
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.api_key_enc IS 'AES-256-GCM encrypted API key (BYTEA)';
COMMENT ON COLUMN provider_configs.key_nonce IS 'AES-GCM nonce for api_key_enc';
COMMENT ON COLUMN provider_configs.key_scope IS 'Encryption tier: global/team use env key, personal uses UEK';
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)';
COMMENT ON COLUMN provider_configs.proxy_mode IS 'system=env proxy, direct=no proxy, custom=explicit URL';
COMMENT ON COLUMN provider_configs.proxy_url IS 'Explicit proxy URL when proxy_mode=custom (http/https/socks5)';
-- =========================================
-- MODEL CATALOG
-- =========================================
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,
model_type VARCHAR(20) DEFAULT 'chat',
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';
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
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 'disabled by default — admin must enable for global models';
COMMENT ON COLUMN model_catalog.model_type IS 'chat, embedding, image — from provider API sync';
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';
-- =========================================
-- MODEL PRICING
-- =========================================
CREATE TABLE IF NOT EXISTS model_pricing (
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,
input_per_m NUMERIC(10,6),
output_per_m NUMERIC(10,6),
cache_create_per_m NUMERIC(10,6),
cache_read_per_m NUMERIC(10,6),
currency TEXT NOT NULL DEFAULT 'USD',
source TEXT NOT NULL DEFAULT 'manual',
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES users(id),
UNIQUE(provider_config_id, model_id)
);
-- =========================================
-- PROVIDER HEALTH
-- =========================================
CREATE TABLE IF NOT EXISTS provider_health (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
window_start TIMESTAMPTZ NOT NULL,
request_count INT NOT NULL DEFAULT 0,
error_count INT NOT NULL DEFAULT 0,
timeout_count INT NOT NULL DEFAULT 0,
total_latency_ms BIGINT NOT NULL DEFAULT 0,
max_latency_ms INT NOT NULL DEFAULT 0,
rate_limit_count INT NOT NULL DEFAULT 0,
last_error TEXT,
last_error_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider_config_id, window_start)
);
CREATE INDEX IF NOT EXISTS idx_provider_health_window
ON provider_health (provider_config_id, window_start DESC);
-- =========================================
-- CAPABILITY OVERRIDES
-- =========================================
CREATE TABLE IF NOT EXISTS capability_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
field TEXT NOT NULL,
value TEXT NOT NULL,
set_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider_config_id, model_id, field)
);
CREATE INDEX IF NOT EXISTS idx_capability_overrides_model
ON capability_overrides (model_id);
-- =========================================
-- ROUTING POLICIES
-- =========================================
CREATE TABLE IF NOT EXISTS routing_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
scope VARCHAR(10) NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team')),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
priority INTEGER NOT NULL DEFAULT 100,
policy_type VARCHAR(30) NOT NULL
CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')),
config JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_routing_policies_active
ON routing_policies(is_active, priority) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_routing_policies_team
ON routing_policies(team_id) WHERE team_id IS NOT NULL;
DROP TRIGGER IF EXISTS routing_policies_updated_at ON routing_policies;
CREATE TRIGGER routing_policies_updated_at BEFORE UPDATE ON routing_policies
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

View File

@@ -1,151 +0,0 @@
-- ==========================================
-- Chat Switchboard — 004 Personas & Grants
-- ==========================================
-- Persona definitions, persona capability grants, and resource access grants.
-- ICD §4 (Personas)
-- ==========================================
-- =========================================
-- PERSONAS
-- =========================================
CREATE TABLE IF NOT EXISTS personas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
handle VARCHAR(50),
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,
-- Memory configuration (v0.18.0)
memory_enabled BOOLEAN NOT NULL DEFAULT true,
memory_extraction_prompt TEXT,
-- 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;
CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL;
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.handle IS 'Unique @mention handle, e.g. veronica-sharpe. Auto-generated from name.';
COMMENT ON COLUMN personas.is_shared IS 'Personal personas shared with others (read-only)';
COMMENT ON COLUMN personas.memory_enabled IS 'Whether memory tools and extraction are active for this persona';
COMMENT ON COLUMN personas.memory_extraction_prompt IS 'Custom extraction prompt; NULL = use system default';
-- =========================================
-- PERSONA GROUPS (v0.23.0)
-- =========================================
-- Saved roster templates. A group is a named set of personas
-- that can be stamped onto any new conversation.
CREATE TABLE IF NOT EXISTS persona_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT DEFAULT '',
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scope VARCHAR(10) NOT NULL DEFAULT 'personal'
CHECK (scope IN ('global', 'team', 'personal')),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id);
DROP TRIGGER IF EXISTS persona_groups_updated_at ON persona_groups;
CREATE TRIGGER persona_groups_updated_at BEFORE UPDATE ON persona_groups
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TABLE IF NOT EXISTS persona_group_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE,
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
is_leader BOOLEAN NOT NULL DEFAULT false,
sort_order INT NOT NULL DEFAULT 0,
UNIQUE(group_id, persona_id)
);
CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id);
-- =========================================
-- PERSONA GRANTS (what a persona can do)
-- =========================================
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 TABLE persona_grants IS 'What a persona can do: tools it can call, KBs it can search, etc.';
COMMENT ON COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base, api_endpoint';
COMMENT ON COLUMN persona_grants.grant_ref IS 'tool: function name. knowledge_base: UUID. api_endpoint: identifier.';
-- =========================================
-- RESOURCE GRANTS (who can USE a resource)
-- =========================================
CREATE TABLE IF NOT EXISTS resource_grants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_type VARCHAR(30) NOT NULL
CHECK (resource_type IN ('persona', 'knowledge_base', 'project')),
resource_id UUID NOT NULL,
grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only'
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
granted_groups UUID[] NOT NULL DEFAULT '{}',
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(resource_type, resource_id)
);
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
ON resource_grants(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
ON resource_grants USING gin(granted_groups);
DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants;
CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE resource_grants IS 'Who can USE a resource (personas, KBs, projects). Cross-team access via groups.';
COMMENT ON COLUMN resource_grants.grant_scope IS 'team_only: existing team behavior. global: all users. groups: specific group list.';

View File

@@ -1,262 +0,0 @@
-- ==========================================
-- Chat Switchboard — 005 Channels & Conversations
-- ==========================================
-- Channels (all types, all columns), messages, participants,
-- models, cursors, folders, user prefs, session participants.
-- Consolidated v0.28.0: merges 005 + 016 (multiuser) + 017 (unread)
-- + 021 (sessions) + 024 (workflow instance cols) + 026 (service type)
-- + v0.28.0 (kb_auto_inject).
--
-- Note: project_id/workspace_id FKs added by 010_projects.sql.
-- Note: workflow_id FK added by 018_workflows.sql.
-- ==========================================
-- =========================================
-- 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 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();
-- =========================================
-- CHANNELS
-- =========================================
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', 'dm', 'group', 'channel', 'workflow', 'service')),
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 REFERENCES folders(id) ON DELETE SET NULL,
folder TEXT, -- legacy text folder (compat)
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
settings JSONB DEFAULT '{}'::jsonb,
tags TEXT[],
-- Multi-user (v0.23.1)
ai_mode VARCHAR(20) NOT NULL DEFAULT 'auto'
CHECK (ai_mode IN ('auto', 'mention_only', 'off')),
topic TEXT,
-- Anonymous sessions (v0.24.3)
allow_anonymous BOOLEAN NOT NULL DEFAULT false,
-- Workflow instance columns (v0.26.0)
-- workflow_id FK added by 018_workflows.sql (deferred — table doesn't exist yet)
workflow_id UUID,
workflow_version INTEGER,
current_stage INTEGER DEFAULT 0,
stage_data JSONB DEFAULT '{}'::jsonb,
workflow_status TEXT DEFAULT 'active'
CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')),
last_activity_at TIMESTAMPTZ DEFAULT NOW(),
stage_entered_at TIMESTAMPTZ,
-- KB auto-injection (v0.28.0)
kb_auto_inject BOOLEAN NOT NULL DEFAULT false,
-- Cross-domain FKs (added by 010_projects.sql)
project_id UUID,
workspace_id UUID,
-- Retention (v0.37.14)
purge_after TIMESTAMPTZ,
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);
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id) WHERE workspace_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id) WHERE workflow_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_workflow_status ON channels(workflow_status) WHERE workflow_status = 'active';
CREATE INDEX IF NOT EXISTS idx_channels_purge_after ON channels(purge_after) WHERE purge_after IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_workflow_active ON channels(workflow_id, workflow_status) WHERE workflow_id IS NOT NULL AND workflow_status = 'active';
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=single-user, dm=human-to-human, group=multi-participant, channel=named persistent, workflow=staged, service=autonomous task';
COMMENT ON COLUMN channels.ai_mode IS 'auto=respond always, mention_only=@mention required, off=AI disabled';
COMMENT ON COLUMN channels.topic IS 'Short channel description shown in header';
COMMENT ON COLUMN channels.allow_anonymous IS 'When true, unauthenticated visitors can join via session token';
COMMENT ON COLUMN channels.workflow_id IS 'FK to workflows — set when type=workflow (constraint added by 018)';
COMMENT ON COLUMN channels.workflow_version IS 'Pinned version number from workflow_versions';
COMMENT ON COLUMN channels.current_stage IS 'Ordinal of the active workflow stage';
COMMENT ON COLUMN channels.stage_data IS 'Accumulated form data collected across stages';
COMMENT ON COLUMN channels.workflow_status IS 'active/completed/stale/cancelled';
COMMENT ON COLUMN channels.last_activity_at IS 'Updated on each message — used by staleness sweep';
COMMENT ON COLUMN channels.stage_entered_at IS 'Timestamp when current workflow stage was entered — used for SLA computation';
COMMENT ON COLUMN channels.kb_auto_inject IS 'When true, top-K KB chunks auto-prepend to context';
-- =========================================
-- MESSAGES
-- =========================================
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),
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
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)';
-- =========================================
-- CHANNEL PARTICIPANTS & MODELS
-- =========================================
CREATE TABLE IF NOT EXISTS channel_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
participant_type VARCHAR(10) NOT NULL DEFAULT 'user'
CHECK (participant_type IN ('user', 'persona', 'session')),
participant_id UUID NOT NULL,
role VARCHAR(10) NOT NULL DEFAULT 'member'
CHECK (role IN ('owner', 'member', 'observer')),
display_name VARCHAR(200),
avatar_url TEXT,
joined_at TIMESTAMPTZ DEFAULT NOW(),
last_read_at TIMESTAMPTZ DEFAULT NOW(),
last_read_message_id UUID, -- v0.23.2: unread cursor
UNIQUE(channel_id, participant_type, participant_id)
);
CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_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,
persona_id UUID REFERENCES personas(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()
);
-- Raw model entries (no persona): unique per channel+model+provider
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw
ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL;
-- Persona entries: one per persona per channel (same model allowed for different personas)
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona
ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
-- =========================================
-- 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);
-- =========================================
-- USER MODEL SETTINGS
-- =========================================
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,
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
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, provider_config_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();
-- =========================================
-- SESSION PARTICIPANTS (v0.24.3)
-- =========================================
CREATE TABLE IF NOT EXISTS session_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_token TEXT NOT NULL UNIQUE,
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
display_name TEXT NOT NULL DEFAULT 'Visitor',
fingerprint TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_session_participants_channel
ON session_participants(channel_id);
CREATE INDEX IF NOT EXISTS idx_session_participants_token
ON session_participants(session_token);
COMMENT ON TABLE session_participants IS 'Ephemeral identities for anonymous workflow channel visitors.';

View File

@@ -1,114 +0,0 @@
-- ==========================================
-- Chat Switchboard — 006 Knowledge Bases
-- ==========================================
-- Knowledge bases, documents, chunks, and bindings to channels/personas.
-- ICD §5 (Knowledge Bases)
-- ==========================================
-- =========================================
-- KNOWLEDGE BASES
-- =========================================
CREATE TABLE IF NOT EXISTS knowledge_bases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT 'global',
owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
embedding_config JSONB NOT NULL DEFAULT '{}',
document_count INT NOT NULL DEFAULT 0,
chunk_count INT NOT NULL DEFAULT 0,
total_bytes BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
discoverable BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT kb_scope_check CHECK (
(scope = 'global' AND owner_id IS NULL) OR
(scope = 'team' AND team_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope);
CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id) WHERE owner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id) WHERE team_id IS NOT NULL;
COMMENT ON COLUMN knowledge_bases.discoverable IS 'false = hidden from user KB lists, only accessible through persona binding';
-- =========================================
-- KB DOCUMENTS
-- =========================================
CREATE TABLE IF NOT EXISTS kb_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
storage_key TEXT NOT NULL,
extracted_text TEXT,
chunk_count INT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
error TEXT,
uploaded_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id);
-- =========================================
-- KB CHUNKS
-- =========================================
CREATE TABLE IF NOT EXISTS kb_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
document_id UUID NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE,
chunk_index INT NOT NULL,
content TEXT NOT NULL,
token_count INT NOT NULL DEFAULT 0,
embedding VECTOR(3072),
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id);
CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id);
-- =========================================
-- CHANNEL ↔ KB BINDING
-- =========================================
CREATE TABLE IF NOT EXISTS channel_knowledge_bases (
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT true,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (channel_id, kb_id)
);
-- =========================================
-- PERSONA ↔ KB BINDING (v0.17.0)
-- =========================================
CREATE TABLE IF NOT EXISTS persona_knowledge_bases (
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
auto_search BOOLEAN NOT NULL DEFAULT false,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (persona_id, kb_id)
);
CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id);
CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id);
COMMENT ON TABLE persona_knowledge_bases IS 'Binds KBs to personas — the persona becomes a gateway to its KBs';
COMMENT ON COLUMN persona_knowledge_bases.auto_search IS 'true = auto-prepend top-K results to context; false = kb_search tool only';

View File

@@ -1,75 +0,0 @@
-- ==========================================
-- Chat Switchboard — 007 Notes & Links
-- ==========================================
-- Notes with full-text search, wikilink graph edges.
-- ICD §6 (Notes)
-- ==========================================
-- =========================================
-- 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,
source_message_id UUID,
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
search_vector TSVECTOR,
embedding VECTOR(3072),
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 INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id)
WHERE source_message_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();
-- =========================================
-- NOTE LINKS (v0.17.3 — wikilink graph)
-- =========================================
CREATE TABLE IF NOT EXISTS note_links (
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
target_note_id UUID REFERENCES notes(id) ON DELETE CASCADE,
target_title TEXT NOT NULL,
display_text TEXT,
is_transclusion BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (source_note_id, target_title)
);
CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id)
WHERE target_note_id IS NOT NULL;
COMMENT ON TABLE note_links IS 'Directed links between notes, extracted from [[wikilink]] syntax on save';
COMMENT ON COLUMN note_links.target_note_id IS 'NULL for unresolved links (target note does not exist yet)';
COMMENT ON COLUMN note_links.target_title IS 'Raw [[title]] text — preserved for re-resolution after renames';
COMMENT ON COLUMN note_links.is_transclusion IS 'true for ![[embed]] syntax, false for regular [[link]]';

View File

@@ -1,89 +0,0 @@
-- ==========================================
-- Chat Switchboard — 008 Memory
-- ==========================================
-- Long-term memory across conversations, extraction tracking.
-- Consolidated v0.28.0: adds last_compacted_at + decay_rate.
-- ==========================================
-- =========================================
-- MEMORIES
-- =========================================
CREATE TABLE IF NOT EXISTS memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')),
owner_id UUID NOT NULL,
user_id UUID,
key TEXT NOT NULL,
value TEXT NOT NULL,
source_channel_id UUID,
confidence REAL NOT NULL DEFAULT 1.0,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'archived')),
embedding vector(3072),
-- Compaction support (v0.28.0)
last_compacted_at TIMESTAMPTZ,
decay_rate REAL NOT NULL DEFAULT 0.0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_memories_scope_owner
ON memories(scope, owner_id, status)
WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_memories_persona_user
ON memories(owner_id, user_id)
WHERE scope = 'persona_user' AND status = 'active';
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key);
CREATE INDEX IF NOT EXISTS idx_memories_fts
ON memories USING gin(to_tsvector('english', key || ' ' || value));
CREATE INDEX IF NOT EXISTS idx_memories_source_channel
ON memories(source_channel_id)
WHERE source_channel_id IS NOT NULL;
COMMENT ON TABLE memories IS 'Long-term memory facts persisted across conversations';
COMMENT ON COLUMN memories.scope IS 'user = personal facts; persona = shared; persona_user = per-user within a persona';
COMMENT ON COLUMN memories.owner_id IS 'user_id for user scope, persona_id for persona/persona_user scopes';
COMMENT ON COLUMN memories.confidence IS '0.0-1.0 confidence score — higher = more certain';
COMMENT ON COLUMN memories.last_compacted_at IS 'Last time this memory was evaluated by the compaction process';
COMMENT ON COLUMN memories.decay_rate IS 'Rate at which confidence decreases over time (0.0 = no decay)';
CREATE OR REPLACE FUNCTION update_memories_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_memories_updated_at ON memories;
CREATE TRIGGER trg_memories_updated_at
BEFORE UPDATE ON memories
FOR EACH ROW EXECUTE FUNCTION update_memories_updated_at();
-- =========================================
-- MEMORY EXTRACTION LOG
-- =========================================
CREATE TABLE IF NOT EXISTS memory_extraction_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL,
user_id UUID NOT NULL,
last_message_id UUID NOT NULL,
extracted_at TIMESTAMPTZ DEFAULT now(),
memory_count INT NOT NULL DEFAULT 0,
UNIQUE(channel_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel
ON memory_extraction_log(channel_id);
COMMENT ON TABLE memory_extraction_log IS 'Tracks extraction progress per channel+user to prevent re-processing';

View File

@@ -1,126 +0,0 @@
-- ==========================================
-- Chat Switchboard — 009 Workspaces & Git
-- ==========================================
-- File workspaces, workspace file index, semantic chunks, git credentials.
-- ICD §7 (Workspaces & Git)
-- ==========================================
-- =========================================
-- WORKSPACES
-- =========================================
CREATE TABLE IF NOT EXISTS workspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_type VARCHAR(20) NOT NULL
CHECK (owner_type IN ('user', 'project', 'channel', 'team')),
owner_id UUID NOT NULL,
name VARCHAR(200) NOT NULL,
root_path TEXT NOT NULL,
max_bytes BIGINT,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'archived', 'deleting')),
indexing_enabled BOOLEAN NOT NULL DEFAULT true,
-- Git integration (v0.21.4)
git_remote_url TEXT,
git_branch TEXT,
git_credential_id UUID,
git_last_sync TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id);
CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status) WHERE status != 'deleting';
DROP TRIGGER IF EXISTS workspaces_updated_at ON workspaces;
CREATE TRIGGER workspaces_updated_at BEFORE UPDATE ON workspaces
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE workspaces IS 'File workspaces bound to users, projects, channels, or teams';
COMMENT ON COLUMN workspaces.root_path IS 'PVC-relative path to workspace root directory';
-- =========================================
-- WORKSPACE FILES
-- =========================================
CREATE TABLE IF NOT EXISTS workspace_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
path TEXT NOT NULL,
is_directory BOOLEAN NOT NULL DEFAULT false,
content_type VARCHAR(100),
size_bytes BIGINT NOT NULL DEFAULT 0,
sha256 VARCHAR(64),
index_status VARCHAR(20) DEFAULT 'pending'
CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')),
chunk_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path
ON workspace_files(workspace_id, path);
CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id);
CREATE INDEX IF NOT EXISTS idx_workspace_files_type
ON workspace_files(workspace_id, content_type) WHERE content_type IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status
ON workspace_files(workspace_id, index_status)
WHERE index_status IN ('pending', 'indexing');
DROP TRIGGER IF EXISTS workspace_files_updated_at ON workspace_files;
CREATE TRIGGER workspace_files_updated_at BEFORE UPDATE ON workspace_files
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE workspace_files IS 'Metadata index for workspace files — filesystem is source of truth';
-- =========================================
-- WORKSPACE CHUNKS (v0.21.2)
-- =========================================
CREATE TABLE IF NOT EXISTS workspace_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
file_id UUID NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE,
chunk_index INT NOT NULL,
content TEXT NOT NULL,
token_count INT NOT NULL DEFAULT 0,
embedding vector(3072),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id);
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id);
COMMENT ON TABLE workspace_chunks IS 'Text chunks with vector embeddings for workspace file semantic search';
-- =========================================
-- GIT CREDENTIALS (v0.21.4)
-- =========================================
CREATE TABLE IF NOT EXISTS git_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT '',
auth_type TEXT NOT NULL DEFAULT 'https_pat',
encrypted_data BYTEA NOT NULL DEFAULT '',
nonce BYTEA NOT NULL DEFAULT '',
public_key TEXT NOT NULL DEFAULT '',
fingerprint TEXT NOT NULL DEFAULT '',
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id);
-- Deferred FK: workspaces.git_credential_id → git_credentials.id
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_workspaces_git_credential'
) THEN
ALTER TABLE workspaces ADD CONSTRAINT fk_workspaces_git_credential
FOREIGN KEY (git_credential_id) REFERENCES git_credentials(id) ON DELETE SET NULL;
END IF;
END $$;

View File

@@ -1,152 +0,0 @@
-- ==========================================
-- Chat Switchboard — 010 Projects
-- ==========================================
-- Projects, junction tables, and cross-domain FK columns on
-- channels/projects for workspace/project bindings.
-- ICD §8 (Projects)
-- ==========================================
-- =========================================
-- PROJECTS
-- =========================================
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
description TEXT,
color VARCHAR(7),
icon VARCHAR(50),
scope VARCHAR(20) NOT NULL DEFAULT 'personal'
CHECK (scope IN ('personal', 'team', 'global')),
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
workspace_id UUID REFERENCES workspaces(id) ON DELETE SET NULL,
is_archived BOOLEAN NOT NULL DEFAULT false,
settings JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id);
CREATE INDEX IF NOT EXISTS idx_projects_team ON projects(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope);
CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id) WHERE workspace_id IS NOT NULL;
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();
COMMENT ON TABLE projects IS 'Organizes channels, KBs, and notes into named workspaces';
COMMENT ON COLUMN projects.scope IS 'personal=owner only, team=team members, global=all users';
-- =========================================
-- PROJECT ↔ CHANNEL JUNCTION
-- =========================================
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,
position INTEGER NOT NULL DEFAULT 0,
folder TEXT,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (project_id, channel_id),
UNIQUE (channel_id)
);
CREATE INDEX IF NOT EXISTS idx_project_channels_project ON project_channels(project_id);
CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id);
COMMENT ON TABLE project_channels IS 'Links channels to projects. UNIQUE(channel_id) enforces one-project-per-channel.';
-- =========================================
-- PROJECT ↔ KB JUNCTION
-- =========================================
CREATE TABLE IF NOT EXISTS project_knowledge_bases (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
auto_search BOOLEAN NOT NULL DEFAULT false,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (project_id, kb_id)
);
CREATE INDEX IF NOT EXISTS idx_project_kb_project ON project_knowledge_bases(project_id);
CREATE INDEX IF NOT EXISTS idx_project_kb_kb ON project_knowledge_bases(kb_id);
-- =========================================
-- PROJECT ↔ NOTE JUNCTION
-- =========================================
CREATE TABLE IF NOT EXISTS project_notes (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (project_id, note_id)
);
CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id);
CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id);
-- =========================================
-- CROSS-DOMAIN FK COLUMNS
-- =========================================
-- Deferred FKs: channels.project_id and workspace_id declared in 005
-- without FK constraints (referenced tables didn't exist yet).
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_project'
) THEN
ALTER TABLE channels ADD CONSTRAINT fk_channels_project
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL;
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workspace'
) THEN
ALTER TABLE channels ADD CONSTRAINT fk_channels_workspace
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL;
END IF;
END $$;
-- =========================================
-- FOLDER → PROJECT MIGRATION (idempotent)
-- =========================================
-- Best-effort migration of legacy folder values into projects.
-- Only runs if there are channels with a folder value and no project_id.
DO $$
DECLARE
_folder TEXT;
_user_id UUID;
_proj_id UUID;
_ch_id UUID;
BEGIN
FOR _user_id, _folder IN
SELECT DISTINCT user_id, folder FROM channels
WHERE folder IS NOT NULL AND folder != '' AND project_id IS NULL
LOOP
INSERT INTO projects (name, scope, owner_id)
VALUES (_folder, 'personal', _user_id)
RETURNING id INTO _proj_id;
FOR _ch_id IN
SELECT id FROM channels
WHERE user_id = _user_id AND folder = _folder AND project_id IS NULL
LOOP
INSERT INTO project_channels (project_id, channel_id, position)
VALUES (_proj_id, _ch_id, 0)
ON CONFLICT DO NOTHING;
UPDATE channels SET project_id = _proj_id WHERE id = _ch_id;
END LOOP;
END LOOP;
END
$$;

View File

@@ -1,8 +0,0 @@
-- ==========================================
-- Chat Switchboard — 012 (placeholder)
-- ==========================================
-- Previously: extensions + extension_user_settings tables.
-- v0.28.7: Both tables removed. Unified package registry in 016.
-- package_user_settings also in 016 (FK dependency on packages).
-- This file is intentionally empty to preserve migration numbering.
-- ==========================================

View File

@@ -1,33 +0,0 @@
-- ==========================================
-- Chat Switchboard — 013 Files
-- ==========================================
-- Unified files table. Fresh install — no legacy attachments migration.
-- Consolidated v0.28.0: removes attachments→files migration dance.
-- ==========================================
CREATE TABLE IF NOT EXISTS files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID REFERENCES channels(id) ON DELETE CASCADE,
message_id UUID REFERENCES messages(id) ON DELETE SET NULL,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
origin VARCHAR(20) NOT NULL DEFAULT 'user_upload'
CHECK (origin IN ('user_upload', 'tool_output', 'system')),
filename VARCHAR(255) NOT NULL,
content_type VARCHAR(127) NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
display_hint VARCHAR(20) NOT NULL DEFAULT 'download'
CHECK (display_hint IN ('inline', 'download', 'thumbnail')),
extracted_text TEXT,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id);
CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id);
CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id);
CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin);
CREATE INDEX IF NOT EXISTS idx_files_orphan ON files(created_at) WHERE message_id IS NULL AND origin = 'user_upload';

View File

@@ -1,58 +0,0 @@
-- ==========================================
-- Chat Switchboard — 014 Audit & Usage
-- ==========================================
-- Audit trail and usage/cost tracking.
-- ICD §16 (Platform Administration), §17 (Export & Utility)
-- ==========================================
-- =========================================
-- 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';
-- =========================================
-- USAGE LOG
-- =========================================
CREATE TABLE IF NOT EXISTS usage_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
provider_scope TEXT NOT NULL DEFAULT 'global',
model_id TEXT NOT NULL,
role TEXT,
prompt_tokens INT NOT NULL DEFAULT 0,
completion_tokens INT NOT NULL DEFAULT 0,
cache_creation_tokens INT NOT NULL DEFAULT 0,
cache_read_tokens INT NOT NULL DEFAULT 0,
cost_input NUMERIC(12,6),
cost_output NUMERIC(12,6),
routing_decision JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at);
CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope);

View File

@@ -1,23 +0,0 @@
-- ==========================================
-- Chat Switchboard — 015 Tool Health
-- ==========================================
-- Health tracking for built-in tools (web_search, url_fetch, etc.).
-- ==========================================
CREATE TABLE IF NOT EXISTS tool_health (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tool_name TEXT NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
request_count INT NOT NULL DEFAULT 0,
error_count INT NOT NULL DEFAULT 0,
total_latency_ms BIGINT NOT NULL DEFAULT 0,
max_latency_ms INT NOT NULL DEFAULT 0,
last_error TEXT,
last_error_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tool_name, window_start)
);
CREATE INDEX IF NOT EXISTS idx_tool_health_window
ON tool_health (tool_name, window_start DESC);

View File

@@ -1,129 +0,0 @@
-- ==========================================
-- Chat Switchboard — 016 Packages
-- ==========================================
-- Unified package registry. Replaces surface_registry (v0.25.0)
-- and extensions (v0.11.0) tables. A package is a surface, an
-- extension, or both.
--
-- v0.28.7: packages table + package_user_settings in 012.
-- v0.29.0: status column + extension_permissions table.
-- v0.29.2: ext_data_tables catalog + platform read views.
-- ==========================================
CREATE TABLE IF NOT EXISTS packages (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'surface'
CHECK (type IN ('surface', 'extension', 'full', 'workflow')),
version TEXT NOT NULL DEFAULT '0.0.0',
description TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
tier TEXT NOT NULL DEFAULT 'browser'
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
is_system BOOLEAN NOT NULL DEFAULT false,
scope TEXT NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team', 'personal')),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
installed_by UUID REFERENCES users(id) ON DELETE SET NULL,
manifest JSONB NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT true,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'suspended')),
schema_version INTEGER NOT NULL DEFAULT 0,
package_settings JSONB NOT NULL DEFAULT '{}',
source TEXT NOT NULL DEFAULT 'core'
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true;
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
COMMENT ON TABLE packages IS 'Unified package registry. Surfaces, extensions, and full packages. Replaces surface_registry + extensions tables.';
COMMENT ON COLUMN packages.id IS 'Slug identifier from manifest "id" field. Used in URLs: /s/:id';
COMMENT ON COLUMN packages.type IS 'surface = routable page, extension = hooks/tools/pipes, full = both, workflow = bundled workflow definition';
COMMENT ON COLUMN packages.source IS 'core = page-engine seeded, builtin = extensions/builtin/ seeded, extension = admin-uploaded .pkg';
COMMENT ON COLUMN packages.enabled IS 'Admin toggle — disabled surfaces redirect to / and hide from nav';
COMMENT ON COLUMN packages.status IS 'Lifecycle: active (running), pending_review (needs admin permission grant), suspended (permission revoked)';
-- =========================================
-- PACKAGE USER SETTINGS
-- =========================================
-- Per-user overrides for packages (enable/disable, custom config).
-- Replaces extension_user_settings from the former extensions schema.
-- Lives in 016 (not 012) because of FK dependency on packages table.
CREATE TABLE IF NOT EXISTS package_user_settings (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
settings JSONB NOT NULL DEFAULT '{}',
is_enabled BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (package_id, user_id)
);
-- =========================================
-- EXTENSION PERMISSIONS
-- =========================================
-- Declared capabilities from package manifests. Admin grants control
-- which modules the Starlark sandbox injects at runtime.
-- v0.29.0: Permission model for extension sandboxing.
CREATE TABLE IF NOT EXISTS extension_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
permission TEXT NOT NULL,
granted BOOLEAN NOT NULL DEFAULT false,
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
granted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(package_id, permission)
);
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true;
COMMENT ON TABLE extension_permissions IS 'Declared permissions from package manifests. Admin grants control runtime module injection.';
COMMENT ON COLUMN extension_permissions.permission IS 'Capability key: secrets.read, notifications.send, filters.pre_completion, db.read, db.write, api.http';
COMMENT ON COLUMN extension_permissions.granted IS 'Admin has reviewed and approved this capability for the package';
-- =========================================
-- EXTENSION DATA TABLES CATALOG
-- =========================================
-- Tracks namespaced tables created for each extension on install.
-- Used by uninstall hooks (DROP TABLE) and db.list_tables().
-- Table names stored without the ext_{id}_ prefix.
-- v0.29.2: Extension data storage.
CREATE TABLE IF NOT EXISTS ext_data_tables (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
table_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (package_id, table_name)
);
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
COMMENT ON TABLE ext_data_tables IS 'Catalog of namespaced tables created by extension install hooks. Drives uninstall cleanup and db.list_tables().';
COMMENT ON COLUMN ext_data_tables.table_name IS 'Logical name (without ext_{id}_ prefix). Physical table is ext_{package_id}_{table_name}.';
-- =========================================
-- PLATFORM VIEWS (extension read access)
-- =========================================
-- Column-allowlisted views over core platform tables.
-- Extensions query these via db.view("users"), db.view("channels").
-- Only safe, non-sensitive columns are exposed.
-- v0.29.2: Read contract for cross-platform data access.
CREATE OR REPLACE VIEW ext_view_users AS
SELECT id, display_name, email FROM users;
CREATE OR REPLACE VIEW ext_view_channels AS
SELECT id, title, type, team_id FROM channels;

View File

@@ -1,18 +0,0 @@
-- ==========================================
-- Chat Switchboard — 017 Auth Providers
-- ==========================================
-- OIDC authorization state tracking (ephemeral).
-- Consolidated v0.28.0: renumbered from 019. groups.source moved to 002.
-- ==========================================
CREATE TABLE IF NOT EXISTS oidc_auth_state (
state TEXT PRIMARY KEY,
nonce TEXT NOT NULL,
redirect_to TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_oidc_state_created
ON oidc_auth_state(created_at);
COMMENT ON TABLE oidc_auth_state IS 'Ephemeral OIDC authorization code flow state. Rows deleted after callback.';

View File

@@ -1,149 +0,0 @@
-- ==========================================
-- Chat Switchboard — 018 Workflows
-- ==========================================
-- Workflow definitions, stages, version snapshots, assignments.
-- Consolidated v0.28.0: merges 023 + 025 (assignments) + 027 (webhooks).
-- Also adds deferred FK: channels.workflow_id → workflows.id.
-- ==========================================
-- =========================================
-- WORKFLOW DEFINITIONS
-- =========================================
CREATE TABLE IF NOT EXISTS workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
branding JSONB NOT NULL DEFAULT '{}',
entry_mode TEXT NOT NULL DEFAULT 'public_link'
CHECK (entry_mode IN ('public_link', 'team_only')),
is_active BOOLEAN NOT NULL DEFAULT false,
version INTEGER NOT NULL DEFAULT 1,
on_complete JSONB,
retention JSONB NOT NULL DEFAULT '{"mode": "archive"}',
-- Webhooks (v0.27.3)
webhook_url TEXT,
webhook_secret TEXT,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
ON workflows(slug) WHERE team_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_workflows_active
ON workflows(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS workflows_updated_at ON workflows;
CREATE TRIGGER workflows_updated_at
BEFORE UPDATE ON workflows
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE workflows IS 'Team-owned or global workflow definitions with staged processes.';
COMMENT ON COLUMN workflows.slug IS 'URL-safe identifier, unique within scope (team or global).';
COMMENT ON COLUMN workflows.branding IS '{"accent_color": "#hex", "logo_url": "...", "tagline": "..."}';
COMMENT ON COLUMN workflows.on_complete IS 'Chaining hook — triggers another workflow on completion.';
COMMENT ON COLUMN workflows.retention IS '{"mode": "archive"|"delete", "delete_after_days": N}';
-- =========================================
-- WORKFLOW STAGES
-- =========================================
CREATE TABLE IF NOT EXISTS workflow_stages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
ordinal INTEGER NOT NULL,
name TEXT NOT NULL,
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
form_template JSONB NOT NULL DEFAULT '{}',
stage_mode TEXT NOT NULL DEFAULT 'chat_only'
CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')),
history_mode TEXT NOT NULL DEFAULT 'full'
CHECK (history_mode IN ('full', 'summary', 'fresh')),
auto_transition BOOLEAN NOT NULL DEFAULT false,
transition_rules JSONB NOT NULL DEFAULT '{}',
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
sla_seconds INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
ON workflow_stages(workflow_id, ordinal);
COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stage has a driving persona and optional human assignment.';
COMMENT ON COLUMN workflow_stages.history_mode IS 'full=complete history, summary=utility-role summary, fresh=clean slate';
COMMENT ON COLUMN workflow_stages.form_template IS 'JSON schema of fields the persona should collect from the visitor.';
COMMENT ON COLUMN workflow_stages.surface_pkg_id IS 'Optional package that provides a custom stage surface. NULL = built-in surface based on stage_mode.';
COMMENT ON COLUMN workflow_stages.sla_seconds IS 'SLA budget in seconds for this stage. NULL = no SLA.';
-- =========================================
-- WORKFLOW VERSIONS (immutable snapshots)
-- =========================================
CREATE TABLE IF NOT EXISTS workflow_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
snapshot JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (workflow_id, version_number)
);
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
ON workflow_versions(workflow_id, version_number DESC);
COMMENT ON TABLE workflow_versions IS 'Immutable snapshots of workflow definitions. Active instances run against a pinned version.';
COMMENT ON COLUMN workflow_versions.snapshot IS 'Full JSON: definition + stages + persona tool grants at publish time.';
-- =========================================
-- WORKFLOW ASSIGNMENTS
-- =========================================
CREATE TABLE IF NOT EXISTS workflow_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
stage INTEGER NOT NULL,
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
assigned_to UUID REFERENCES users(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'unassigned'
CHECK (status IN ('unassigned', 'claimed', 'completed')),
review_comments JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
claimed_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel
ON workflow_assignments(channel_id, stage);
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status
ON workflow_assignments(team_id, status) WHERE status = 'unassigned';
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_user
ON workflow_assignments(assigned_to) WHERE assigned_to IS NOT NULL;
COMMENT ON TABLE workflow_assignments IS 'Queue entries for human review stages in workflows.';
COMMENT ON COLUMN workflow_assignments.status IS 'unassigned=waiting, claimed=being worked, completed=done';
-- =========================================
-- DEFERRED FK: channels.workflow_id → workflows
-- =========================================
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workflow'
) THEN
ALTER TABLE channels ADD CONSTRAINT fk_channels_workflow
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL;
END IF;
END $$;

View File

@@ -1,37 +0,0 @@
-- ==========================================
-- Chat Switchboard — 020 Multi-Replica HA
-- ==========================================
-- Shared state tables for multi-replica operation.
-- v0.32.0: ws_tickets, rate_limit_counters.
-- ==========================================
-- =========================================
-- WEBSOCKET TICKETS (replaces in-memory sync.Map)
-- =========================================
-- Short-lived, single-use tokens for WebSocket authentication.
-- Ticket issued on pod-1 must validate on pod-2.
CREATE TABLE IF NOT EXISTS ws_tickets (
id TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires
ON ws_tickets (expires_at);
-- =========================================
-- RATE LIMIT COUNTERS (replaces in-memory token bucket)
-- =========================================
-- Fixed-window counters keyed by scope:identifier and time bucket.
-- Key format: "{scope}:{identifier}" e.g. "auth:192.168.1.1"
CREATE TABLE IF NOT EXISTS rate_limit_counters (
key TEXT NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
tokens REAL NOT NULL DEFAULT 0,
PRIMARY KEY (key, window_start)
);
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
ON rate_limit_counters (window_start);

View File

@@ -1,18 +0,0 @@
-- ==========================================
-- Chat Switchboard — 021 Data Portability
-- ==========================================
-- v0.34.0: Indexes for data export and GDPR operations.
-- ==========================================
-- Partial index for batch message export (only non-deleted)
CREATE INDEX IF NOT EXISTS idx_messages_channel_active
ON messages (channel_id, created_at)
WHERE deleted_at IS NULL;
-- Index for user-scoped channel listing (export)
CREATE INDEX IF NOT EXISTS idx_channels_user_id
ON channels (user_id);
-- Index for file lookup by user (export)
CREATE INDEX IF NOT EXISTS idx_files_user_id
ON files (user_id);

View File

@@ -1,37 +0,0 @@
-- ==========================================
-- Chat Switchboard — 022 Extension Connections
-- ==========================================
-- v0.38.1: Scoped credential management for extensions.
-- Same scope/resolution pattern as provider_configs.
-- ==========================================
CREATE TABLE IF NOT EXISTS ext_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL,
package_id TEXT NOT NULL,
scope VARCHAR(10) NOT NULL
CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(type, scope, owner_id, name)
);
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != '';
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections;
CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE ext_connections IS 'v0.38.1: Extension connection credentials (scoped, encrypted secrets in config JSON)';
COMMENT ON COLUMN ext_connections.type IS 'Connection type identifier, shared across packages (e.g. "gitea", "github")';
COMMENT ON COLUMN ext_connections.package_id IS 'Declaring package ID (UI attribution only, not access control)';
COMMENT ON COLUMN ext_connections.scope IS 'global=admin-managed, team=team-scoped, personal=user-owned';
COMMENT ON COLUMN ext_connections.owner_id IS 'Empty for global; team_id for team scope; user_id for personal scope';
COMMENT ON COLUMN ext_connections.config IS 'JSON config blob; secret-type fields encrypted at rest by handler layer';

View File

@@ -1,31 +0,0 @@
-- ==========================================
-- Chat Switchboard — 023 Extension Dependencies
-- ==========================================
-- v0.38.2: Library package dependency tracking.
-- Consumers declare which libraries they depend on.
-- Libraries with active consumers cannot be uninstalled.
-- ==========================================
-- Add 'library' to the packages type constraint.
ALTER TABLE packages DROP CONSTRAINT IF EXISTS packages_type_check;
ALTER TABLE packages ADD CONSTRAINT packages_type_check
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library'));
CREATE TABLE IF NOT EXISTS ext_dependencies (
consumer_id TEXT NOT NULL,
library_id TEXT NOT NULL,
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
PRIMARY KEY (consumer_id, library_id),
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
COMMENT ON TABLE ext_dependencies IS 'v0.38.2: Package dependency graph — consumers → libraries';
COMMENT ON COLUMN ext_dependencies.consumer_id IS 'Package that depends on the library';
COMMENT ON COLUMN ext_dependencies.library_id IS 'Library package being depended upon';
COMMENT ON COLUMN ext_dependencies.version_spec IS 'Semver version spec declared by consumer (e.g. ">=1.0.0")';
COMMENT ON COLUMN ext_dependencies.resolved_ver IS 'Actual library version at dependency creation time';

View File

@@ -1,17 +1,10 @@
-- ==========================================
-- Chat Switchboard — 001 Core
-- Switchboard Core — 001 Core
-- ==========================================
-- Users (with auth fields), refresh tokens, platform policies,
-- global settings, user presence.
-- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence).
-- Users, auth, platform policies, global settings, presence, OIDC.
-- ==========================================
-- ── Extensions ──────────────────────────────
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector (KB embeddings)
-- ── Utility: auto-update updated_at ─────────
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
@@ -21,47 +14,37 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
-- =========================================
-- USERS
-- =========================================
-- ── Users ───────────────────────────────────
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash TEXT, -- nullable for mTLS/OIDC
password_hash TEXT,
display_name VARCHAR(100),
avatar_url TEXT,
role VARCHAR(20) DEFAULT 'user'
CHECK (role IN ('user', 'admin')),
is_active BOOLEAN DEFAULT true,
settings JSONB DEFAULT '{}'::jsonb,
-- Auth abstraction (v0.24.0)
auth_source VARCHAR(20) NOT NULL DEFAULT 'builtin'
CHECK (auth_source IN ('builtin', 'mtls', 'oidc')),
external_id TEXT,
handle VARCHAR(100) NOT NULL,
-- Vault: per-user encryption key (UEK) for BYOK API keys
encrypted_uek BYTEA,
uek_salt BYTEA,
uek_nonce BYTEA,
vault_set BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_login_at TIMESTAMPTZ
);
-- Case-insensitive unique indexes
CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (LOWER(username));
CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (LOWER(email));
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(LOWER(handle));
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id
ON users(auth_source, external_id) WHERE external_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
@@ -69,18 +52,7 @@ 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();
COMMENT ON COLUMN users.auth_source IS 'builtin=password, mtls=cert, oidc=SSO';
COMMENT ON COLUMN users.external_id IS 'IdP subject claim (OIDC) or cert fingerprint (mTLS)';
COMMENT ON COLUMN users.handle IS 'Unique @mention handle, auto-generated from username';
COMMENT ON COLUMN users.encrypted_uek IS 'UEK encrypted with Argon2id(password)-derived key';
COMMENT ON COLUMN users.uek_salt IS 'Argon2id salt for UEK derivation';
COMMENT ON COLUMN users.uek_nonce IS 'AES-GCM nonce for UEK wrapping';
COMMENT ON COLUMN users.vault_set IS 'true once UEK has been generated and wrapped';
-- =========================================
-- AUTH — REFRESH TOKENS
-- =========================================
-- ── Refresh Tokens ──────────────────────────
CREATE TABLE IF NOT EXISTS refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -94,10 +66,7 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
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;
-- =========================================
-- PLATFORM POLICIES
-- =========================================
-- ── Platform Policies ───────────────────────
CREATE TABLE IF NOT EXISTS platform_policies (
key VARCHAR(50) PRIMARY KEY,
@@ -107,21 +76,11 @@ CREATE TABLE IF NOT EXISTS platform_policies (
);
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'),
('kb_direct_access', 'true')
('allow_registration', 'true'),
('default_user_active', 'false')
ON CONFLICT (key) DO NOTHING;
COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior';
-- =========================================
-- GLOBAL SETTINGS
-- =========================================
-- ── Global Settings ─────────────────────────
CREATE TABLE IF NOT EXISTS global_settings (
key VARCHAR(100) PRIMARY KEY,
@@ -132,20 +91,11 @@ CREATE TABLE IF NOT EXISTS global_settings (
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": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb),
('model_roles', '{
"utility": { "primary": null, "fallback": null },
"embedding": { "primary": null, "fallback": null },
"generation": { "primary": null, "fallback": null }
}'::jsonb),
('retention_ttl_days', '{"value": 0}'::jsonb)
('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'::jsonb),
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb)
ON CONFLICT (key) DO NOTHING;
-- =========================================
-- USER PRESENCE (v0.23.1)
-- =========================================
-- ── User Presence ───────────────────────────
CREATE TABLE IF NOT EXISTS user_presence (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
@@ -156,4 +106,13 @@ CREATE TABLE IF NOT EXISTS user_presence (
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);
COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';
-- ── OIDC Auth State ─────────────────────────
CREATE TABLE IF NOT EXISTS oidc_auth_state (
state TEXT PRIMARY KEY,
nonce TEXT NOT NULL,
redirect_to TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);

View File

@@ -1,14 +1,6 @@
-- ==========================================
-- Chat Switchboard — 002 Teams & Access Control
-- Switchboard Core — 002 Teams & Access Control
-- ==========================================
-- Teams, team membership, groups (with permissions, budgets, source),
-- group membership, Everyone group seed.
-- Consolidated v0.28.0: merges 002 + 019 (source) + 020 (permissions).
-- ==========================================
-- =========================================
-- TEAMS
-- =========================================
CREATE TABLE IF NOT EXISTS teams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -26,8 +18,6 @@ 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,
@@ -41,10 +31,7 @@ CREATE TABLE IF NOT EXISTS team_members (
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);
-- =========================================
-- GROUPS (ACLs + Permissions)
-- =========================================
-- ── Groups ──────────────────────────────────
CREATE TABLE IF NOT EXISTS groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -53,19 +40,15 @@ CREATE TABLE IF NOT EXISTS groups (
scope VARCHAR(20) NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team')),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
created_by UUID REFERENCES users(id), -- nullable for system-seeded
created_by UUID REFERENCES users(id),
source VARCHAR(20) NOT NULL DEFAULT 'manual'
CHECK (source IN ('manual', 'oidc', 'system')),
-- Fine-grained permissions (v0.24.2)
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
token_budget_daily BIGINT,
token_budget_monthly BIGINT,
allowed_models JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT groups_scope_team CHECK (
(scope = 'global' AND team_id IS NULL) OR
(scope = 'team' AND team_id IS NOT NULL)
@@ -74,19 +57,10 @@ CREATE TABLE IF NOT EXISTS groups (
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));
DROP TRIGGER IF EXISTS groups_updated_at ON groups;
CREATE TRIGGER groups_updated_at BEFORE UPDATE ON groups
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE groups IS 'Access-control groups with permission grants. Decouple resource visibility from team membership.';
COMMENT ON COLUMN groups.scope IS 'global: admin-managed. team: team-admin-managed.';
COMMENT ON COLUMN groups.source IS 'manual=admin-created, oidc=synced from IdP, system=platform-seeded';
COMMENT ON COLUMN groups.permissions IS 'Array of permission strings granted to group members';
COMMENT ON COLUMN groups.token_budget_daily IS 'Daily token ceiling for members (NULL = unlimited)';
COMMENT ON COLUMN groups.token_budget_monthly IS 'Monthly token ceiling for members (NULL = unlimited)';
COMMENT ON COLUMN groups.allowed_models IS 'Array of model_id strings this group may use. NULL = unrestricted.';
CREATE TABLE IF NOT EXISTS group_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
@@ -99,15 +73,13 @@ CREATE TABLE IF NOT EXISTS group_members (
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
-- Seed the Everyone group with a stable well-known ID.
-- Seed Everyone group
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES (
'00000000-0000-0000-0000-000000000001',
'Everyone',
'Implicit group — all authenticated users receive these permissions.',
'global',
NULL,
'system',
'["model.use","kb.read","channel.create"]'::jsonb
'global', NULL, 'system',
'["extension.use","workflow.submit"]'::jsonb
)
ON CONFLICT (id) DO NOTHING;

View File

@@ -0,0 +1,78 @@
-- ==========================================
-- Switchboard Core — 003 Packages & Extensions
-- ==========================================
CREATE TABLE IF NOT EXISTS packages (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'surface'
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
version TEXT NOT NULL DEFAULT '0.0.0',
description TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
tier TEXT NOT NULL DEFAULT 'browser'
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
is_system BOOLEAN NOT NULL DEFAULT false,
scope TEXT NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team', 'personal')),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
installed_by UUID REFERENCES users(id) ON DELETE SET NULL,
manifest JSONB NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT true,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'suspended')),
schema_version INTEGER NOT NULL DEFAULT 0,
package_settings JSONB NOT NULL DEFAULT '{}',
source TEXT NOT NULL DEFAULT 'core'
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true;
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
-- ── Package User Settings ───────────────────
CREATE TABLE IF NOT EXISTS package_user_settings (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
settings JSONB NOT NULL DEFAULT '{}',
is_enabled BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (package_id, user_id)
);
-- ── Extension Permissions ───────────────────
CREATE TABLE IF NOT EXISTS extension_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
permission TEXT NOT NULL,
granted BOOLEAN NOT NULL DEFAULT false,
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
granted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(package_id, permission)
);
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true;
-- ── Extension Data Tables Catalog ───────────
CREATE TABLE IF NOT EXISTS ext_data_tables (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
table_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (package_id, table_name)
);
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
-- ── Platform Read View ──────────────────────
CREATE OR REPLACE VIEW ext_view_users AS
SELECT id, display_name, email FROM users;

View File

@@ -0,0 +1,66 @@
-- ==========================================
-- Switchboard Core — 004 Connections & Dependencies
-- ==========================================
CREATE TABLE IF NOT EXISTS ext_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL,
package_id TEXT NOT NULL,
scope VARCHAR(10) NOT NULL
CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(type, scope, owner_id, name)
);
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != '';
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections;
CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ── Extension Dependencies ──────────────────
CREATE TABLE IF NOT EXISTS ext_dependencies (
consumer_id TEXT NOT NULL,
library_id TEXT NOT NULL,
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
PRIMARY KEY (consumer_id, library_id),
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
-- ── Resource Grants ─────────────────────────
CREATE TABLE IF NOT EXISTS resource_grants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_type VARCHAR(30) NOT NULL,
resource_id UUID NOT NULL,
grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only'
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
granted_groups UUID[] NOT NULL DEFAULT '{}',
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(resource_type, resource_id)
);
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
ON resource_grants(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
ON resource_grants USING gin(granted_groups);
DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants;
CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

View File

@@ -1,13 +1,6 @@
-- ==========================================
-- Chat Switchboard — 011 Notifications
-- Switchboard Core — 005 Notifications
-- ==========================================
-- Notification delivery and per-user preferences.
-- ICD §12 (Notifications)
-- ==========================================
-- =========================================
-- NOTIFICATIONS
-- =========================================
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -21,13 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications (
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at DESC);
-- =========================================
-- NOTIFICATION PREFERENCES
-- =========================================
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread
ON notifications(user_id, is_read, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_user_created
ON notifications(user_id, created_at DESC);
CREATE TABLE IF NOT EXISTS notification_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

View File

@@ -0,0 +1,20 @@
-- ==========================================
-- Switchboard Core — 006 Audit
-- ==========================================
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);

View File

@@ -0,0 +1,76 @@
-- ==========================================
-- Switchboard Core — 007 Workflows
-- ==========================================
-- Workflow definitions, stages, version snapshots.
-- persona_id kept as nullable TEXT (no FK — personas are extensions now).
-- workflow_assignments dropped (channel-dependent, rebuild as needed).
-- ==========================================
CREATE TABLE IF NOT EXISTS workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
branding JSONB NOT NULL DEFAULT '{}',
entry_mode TEXT NOT NULL DEFAULT 'public_link'
CHECK (entry_mode IN ('public_link', 'team_only')),
is_active BOOLEAN NOT NULL DEFAULT false,
version INTEGER NOT NULL DEFAULT 1,
on_complete JSONB,
retention JSONB NOT NULL DEFAULT '{"mode": "archive"}',
webhook_url TEXT,
webhook_secret TEXT,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
ON workflows(slug) WHERE team_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_workflows_active
ON workflows(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS workflows_updated_at ON workflows;
CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ── Workflow Stages ─────────────────────────
CREATE TABLE IF NOT EXISTS workflow_stages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
ordinal INTEGER NOT NULL,
name TEXT NOT NULL,
persona_id TEXT,
assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
form_template JSONB NOT NULL DEFAULT '{}',
stage_mode TEXT NOT NULL DEFAULT 'form_only'
CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')),
history_mode TEXT NOT NULL DEFAULT 'full'
CHECK (history_mode IN ('full', 'summary', 'fresh')),
auto_transition BOOLEAN NOT NULL DEFAULT false,
transition_rules JSONB NOT NULL DEFAULT '{}',
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
sla_seconds INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
ON workflow_stages(workflow_id, ordinal);
-- ── Workflow Versions ───────────────────────
CREATE TABLE IF NOT EXISTS workflow_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
snapshot JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (workflow_id, version_number)
);
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
ON workflow_versions(workflow_id, version_number DESC);

View File

@@ -1,15 +1,11 @@
-- ==========================================
-- Chat Switchboard — 019 Tasks
-- Switchboard Core — 008 Tasks
-- ==========================================
-- Task definitions and run history for autonomous agents.
-- Consolidated v0.28.0: merges 026 + 027 (webhook_secret inline).
-- v0.28.0-cs3: trigger_token, action task_type, queued run status.
-- Task scheduling and run history.
-- Stripped: persona_id (kept nullable, no FK), output_channel_id,
-- provider_config_id. Output modes: notification | webhook | log.
-- ==========================================
-- =========================================
-- TASK DEFINITIONS
-- =========================================
CREATE TABLE IF NOT EXISTS tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -18,44 +14,28 @@ CREATE TABLE IF NOT EXISTS tasks (
description TEXT DEFAULT '',
scope TEXT NOT NULL DEFAULT 'personal'
CHECK (scope IN ('personal', 'team', 'global')),
-- What to run
task_type TEXT NOT NULL DEFAULT 'prompt'
CHECK (task_type IN ('prompt', 'workflow', 'action', 'system')),
task_type TEXT NOT NULL DEFAULT 'action'
CHECK (task_type IN ('action', 'workflow', 'system')),
system_function TEXT DEFAULT '',
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
persona_id TEXT,
model_id TEXT,
system_prompt TEXT DEFAULT '',
user_prompt TEXT DEFAULT '',
workflow_id UUID REFERENCES workflows(id) ON DELETE SET NULL,
tool_grants JSONB,
-- Schedule: cron expression, "once", or "webhook"
schedule TEXT NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC',
is_active BOOLEAN NOT NULL DEFAULT true,
-- Webhook trigger (inbound) — token-based auth for /hooks/t/:token
trigger_token TEXT UNIQUE,
-- Execution policy
max_tokens INTEGER NOT NULL DEFAULT 4096,
max_tool_calls INTEGER NOT NULL DEFAULT 10,
max_wall_clock INTEGER NOT NULL DEFAULT 300,
output_mode TEXT NOT NULL DEFAULT 'channel'
CHECK (output_mode IN ('channel', 'note', 'webhook')),
output_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
output_mode TEXT NOT NULL DEFAULT 'log'
CHECK (output_mode IN ('notification', 'webhook', 'log')),
webhook_url TEXT,
webhook_secret TEXT,
-- Provider routing
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
-- Notifications
notify_on_complete BOOLEAN NOT NULL DEFAULT false,
notify_on_failure BOOLEAN NOT NULL DEFAULT true,
-- Bookkeeping
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
run_count INTEGER NOT NULL DEFAULT 0,
@@ -63,20 +43,17 @@ CREATE TABLE IF NOT EXISTS tasks (
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id);
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id);
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token
ON tasks(trigger_token) WHERE trigger_token IS NOT NULL;
-- =========================================
-- TASK RUN HISTORY
-- =========================================
-- ── Task Runs ───────────────────────────────
CREATE TABLE IF NOT EXISTS task_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'running'
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')),
trigger_payload TEXT,
@@ -88,5 +65,5 @@ CREATE TABLE IF NOT EXISTS task_runs (
error TEXT
);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status);

View File

@@ -0,0 +1,21 @@
-- ==========================================
-- Switchboard Core — 009 Multi-Replica HA
-- ==========================================
CREATE TABLE IF NOT EXISTS ws_tickets (
id TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at);
CREATE TABLE IF NOT EXISTS rate_limit_counters (
key TEXT NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
tokens REAL NOT NULL DEFAULT 0,
PRIMARY KEY (key, window_start)
);
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
ON rate_limit_counters(window_start);

View File

@@ -1,7 +1,6 @@
-- ==========================================
-- Chat Switchboard — 001 Core (SQLite)
-- Switchboard Core — 001 Core (SQLite)
-- ==========================================
-- Consolidated v0.28.0
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
@@ -59,13 +58,8 @@ CREATE TABLE IF NOT EXISTS platform_policies (
);
INSERT OR IGNORE 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'),
('kb_direct_access', 'true');
('allow_registration', 'true'),
('default_user_active', 'false');
CREATE TABLE IF NOT EXISTS global_settings (
key TEXT PRIMARY KEY,
@@ -76,15 +70,23 @@ CREATE TABLE IF NOT EXISTS global_settings (
INSERT OR IGNORE INTO global_settings (key, value) VALUES
('registration', '{"enabled": true}'),
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'),
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'),
('model_roles', '{"utility":{"primary":null,"fallback":null},"embedding":{"primary":null,"fallback":null},"generation":{"primary":null,"fallback":null}}'),
('retention_ttl_days', '{"value": 0}');
('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'),
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}');
CREATE TABLE IF NOT EXISTS user_presence (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_seen TEXT NOT NULL DEFAULT (datetime('now')),
status TEXT NOT NULL DEFAULT 'online'
CHECK (status IN ('online', 'away', 'offline'))
);
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen);
CREATE TABLE IF NOT EXISTS oidc_auth_state (
state TEXT PRIMARY KEY,
nonce TEXT NOT NULL,
redirect_to TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);

View File

@@ -1,11 +1,12 @@
-- Chat Switchboard — 002 Teams & Access Control (SQLite)
-- Consolidated v0.28.0
-- ==========================================
-- Switchboard Core — 002 Teams & Access Control (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_by TEXT NOT NULL REFERENCES users(id),
is_active INTEGER DEFAULT 1,
settings TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
@@ -14,6 +15,10 @@ CREATE TABLE IF NOT EXISTS teams (
CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active);
CREATE TRIGGER IF NOT EXISTS teams_updated_at AFTER UPDATE ON teams
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE teams SET updated_at = datetime('now') WHERE id = NEW.id; END;
CREATE TABLE IF NOT EXISTS team_members (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
@@ -43,7 +48,11 @@ CREATE TABLE IF NOT EXISTS groups (
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
ON groups(name, COALESCE(team_id, ''));
ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));
CREATE TRIGGER IF NOT EXISTS groups_updated_at AFTER UPDATE ON groups
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE groups SET updated_at = datetime('now') WHERE id = NEW.id; END;
CREATE TABLE IF NOT EXISTS group_members (
id TEXT PRIMARY KEY,
@@ -57,13 +66,11 @@ CREATE TABLE IF NOT EXISTS group_members (
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
INSERT OR IGNORE INTO groups
(id, name, description, scope, created_by, source, permissions, created_at, updated_at)
INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES (
'00000000-0000-0000-0000-000000000001',
'Everyone',
'Implicit group — all authenticated users receive these permissions.',
'global', NULL, 'system',
'["model.use","kb.read","channel.create"]',
datetime('now'), datetime('now')
'["extension.use","workflow.submit"]'
);

View File

@@ -1,14 +1,12 @@
-- Chat Switchboard — 016 Packages (SQLite)
-- Unified package registry. Replaces surface_registry + extensions.
-- v0.28.7: packages + package_user_settings
-- v0.29.0: status column + extension_permissions table
-- v0.29.2: ext_data_tables catalog + platform read views
-- ==========================================
-- Switchboard Core — 003 Packages & Extensions (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS packages (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'surface'
CHECK (type IN ('surface', 'extension', 'full', 'workflow')),
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
version TEXT NOT NULL DEFAULT '0.0.0',
description TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
@@ -37,20 +35,14 @@ CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
-- Per-user overrides for packages (enable/disable, custom config).
CREATE TABLE IF NOT EXISTS package_user_settings (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
settings TEXT NOT NULL DEFAULT '{}',
settings TEXT NOT NULL DEFAULT '{}',
is_enabled INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (package_id, user_id)
);
-- Extension permissions (v0.29.0)
CREATE TABLE IF NOT EXISTS extension_permissions (
id TEXT PRIMARY KEY,
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
@@ -65,10 +57,6 @@ CREATE TABLE IF NOT EXISTS extension_permissions (
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted);
-- Extension data tables catalog (v0.29.2)
-- Tracks namespaced tables created by extension install hooks.
CREATE TABLE IF NOT EXISTS ext_data_tables (
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
table_name TEXT NOT NULL,
@@ -78,14 +66,5 @@ CREATE TABLE IF NOT EXISTS ext_data_tables (
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
-- Platform views — column-allowlisted read access for extensions (v0.29.2)
-- Extensions query these via db.view("users"), db.view("channels").
DROP VIEW IF EXISTS ext_view_users;
CREATE VIEW ext_view_users AS
CREATE VIEW IF NOT EXISTS ext_view_users AS
SELECT id, display_name, email FROM users;
DROP VIEW IF EXISTS ext_view_channels;
CREATE VIEW ext_view_channels AS
SELECT id, title, type, team_id FROM channels;

View File

@@ -1,110 +0,0 @@
-- Chat Switchboard — 003 Providers & Routing (SQLite)
-- Consolidated v0.28.0: adds capability_match policy type
CREATE TABLE IF NOT EXISTS provider_configs (
id TEXT PRIMARY KEY,
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT,
name TEXT NOT NULL,
provider TEXT NOT NULL,
endpoint TEXT NOT NULL,
api_key_enc BLOB,
key_nonce BLOB,
key_scope TEXT NOT NULL DEFAULT 'global',
model_default TEXT,
config TEXT DEFAULT '{}',
headers TEXT DEFAULT '{}',
settings TEXT DEFAULT '{}',
is_active INTEGER DEFAULT 1,
is_private INTEGER DEFAULT 0,
proxy_mode TEXT NOT NULL DEFAULT 'system' CHECK (proxy_mode IN ('system', 'direct', 'custom')),
proxy_url TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('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);
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active);
CREATE TABLE IF NOT EXISTS model_catalog (
id TEXT PRIMARY KEY,
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
display_name TEXT,
model_type TEXT DEFAULT 'chat',
capabilities TEXT NOT NULL DEFAULT '{}',
pricing TEXT,
visibility TEXT DEFAULT 'disabled' CHECK (visibility IN ('enabled', 'disabled', 'team')),
last_synced_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('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);
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
CREATE TABLE IF NOT EXISTS model_pricing (
id TEXT PRIMARY KEY,
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
input_per_m REAL,
output_per_m REAL,
cache_create_per_m REAL,
cache_read_per_m REAL,
currency TEXT NOT NULL DEFAULT 'USD',
source TEXT NOT NULL DEFAULT 'manual',
updated_at TEXT DEFAULT (datetime('now')),
updated_by TEXT REFERENCES users(id),
UNIQUE(provider_config_id, model_id)
);
CREATE TABLE IF NOT EXISTS provider_health (
id TEXT PRIMARY KEY,
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
window_start TEXT NOT NULL,
request_count INTEGER NOT NULL DEFAULT 0,
error_count INTEGER NOT NULL DEFAULT 0,
timeout_count INTEGER NOT NULL DEFAULT 0,
total_latency_ms INTEGER NOT NULL DEFAULT 0,
max_latency_ms INTEGER NOT NULL DEFAULT 0,
rate_limit_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
last_error_at TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (provider_config_id, window_start)
);
CREATE INDEX IF NOT EXISTS idx_provider_health_window
ON provider_health (provider_config_id, window_start DESC);
CREATE TABLE IF NOT EXISTS capability_overrides (
id TEXT PRIMARY KEY,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
field TEXT NOT NULL,
value TEXT NOT NULL,
set_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (provider_config_id, model_id, field)
);
CREATE INDEX IF NOT EXISTS idx_capability_overrides_model ON capability_overrides(model_id);
CREATE TABLE IF NOT EXISTS routing_policies (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
scope TEXT NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')),
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
priority INTEGER NOT NULL DEFAULT 100,
policy_type TEXT NOT NULL
CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')),
config TEXT NOT NULL DEFAULT '{}',
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_routing_policies_active ON routing_policies(is_active, priority);

View File

@@ -0,0 +1,59 @@
-- ==========================================
-- Switchboard Core — 004 Connections & Dependencies (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS ext_connections (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
package_id TEXT NOT NULL,
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}',
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(type, scope, owner_id, name)
);
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id);
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active);
CREATE TRIGGER IF NOT EXISTS ext_connections_updated_at AFTER UPDATE ON ext_connections
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE ext_connections SET updated_at = datetime('now') WHERE id = NEW.id; END;
CREATE TABLE IF NOT EXISTS ext_dependencies (
consumer_id TEXT NOT NULL,
library_id TEXT NOT NULL,
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
PRIMARY KEY (consumer_id, library_id),
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES packages(id)
);
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
CREATE TABLE IF NOT EXISTS resource_grants (
id TEXT PRIMARY KEY,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
grant_scope TEXT NOT NULL DEFAULT 'team_only'
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
granted_groups TEXT NOT NULL DEFAULT '[]',
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(resource_type, resource_id)
);
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
ON resource_grants(resource_type, resource_id);
CREATE TRIGGER IF NOT EXISTS resource_grants_updated_at AFTER UPDATE ON resource_grants
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE resource_grants SET updated_at = datetime('now') WHERE id = NEW.id; END;

View File

@@ -1,83 +0,0 @@
-- Chat Switchboard — 004 Personas & Grants (SQLite)
CREATE TABLE IF NOT EXISTS personas (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
handle TEXT,
description TEXT DEFAULT '',
icon TEXT DEFAULT '',
avatar TEXT DEFAULT '',
base_model_id TEXT NOT NULL,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
system_prompt TEXT DEFAULT '',
temperature REAL,
max_tokens INTEGER,
thinking_budget INTEGER,
top_p REAL,
memory_enabled INTEGER NOT NULL DEFAULT 1,
memory_extraction_prompt TEXT,
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT,
created_by TEXT NOT NULL REFERENCES users(id),
is_active INTEGER DEFAULT 1,
is_shared INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope);
CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id);
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active);
CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL;
CREATE TABLE IF NOT EXISTS persona_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('global', 'team', 'personal')),
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id);
CREATE TABLE IF NOT EXISTS persona_group_members (
id TEXT PRIMARY KEY,
group_id TEXT NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE,
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
is_leader INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
UNIQUE(group_id, persona_id)
);
CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id);
CREATE TABLE IF NOT EXISTS persona_grants (
id TEXT PRIMARY KEY,
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
grant_type TEXT NOT NULL,
grant_ref TEXT NOT NULL,
config TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('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);
CREATE TABLE IF NOT EXISTS resource_grants (
id TEXT PRIMARY KEY,
resource_type TEXT NOT NULL CHECK (resource_type IN ('persona', 'knowledge_base', 'project')),
resource_id TEXT NOT NULL,
grant_scope TEXT NOT NULL DEFAULT 'team_only'
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
granted_groups TEXT NOT NULL DEFAULT '[]',
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(resource_type, resource_id)
);
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource ON resource_grants(resource_type, resource_id);

View File

@@ -1,160 +0,0 @@
-- Chat Switchboard — 005 Channels & Conversations (SQLite)
-- Consolidated v0.28.0
CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
parent_id TEXT REFERENCES folders(id) ON DELETE CASCADE,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('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);
CREATE TABLE IF NOT EXISTS channels (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
type TEXT DEFAULT 'direct'
CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service')),
model TEXT,
system_prompt TEXT,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
is_archived INTEGER DEFAULT 0,
is_pinned INTEGER DEFAULT 0,
folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
folder TEXT,
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
settings TEXT DEFAULT '{}',
tags TEXT DEFAULT '[]',
ai_mode TEXT NOT NULL DEFAULT 'auto',
topic TEXT,
allow_anonymous INTEGER NOT NULL DEFAULT 0,
workflow_id TEXT,
workflow_version INTEGER,
current_stage INTEGER DEFAULT 0,
stage_data TEXT DEFAULT '{}',
workflow_status TEXT DEFAULT 'active'
CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')),
last_activity_at TEXT DEFAULT (datetime('now')),
stage_entered_at TEXT,
kb_auto_inject INTEGER NOT NULL DEFAULT 0,
project_id TEXT,
workspace_id TEXT,
purge_after TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('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);
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id);
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id);
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id);
CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id);
CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
content TEXT NOT NULL,
model TEXT,
tokens_used INTEGER,
tool_calls TEXT,
metadata TEXT DEFAULT '{}',
parent_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
sibling_index INTEGER DEFAULT 0,
participant_type TEXT DEFAULT 'user',
participant_id TEXT,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
deleted_at TEXT,
created_at TEXT DEFAULT (datetime('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 TABLE IF NOT EXISTS channel_participants (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
participant_type TEXT NOT NULL DEFAULT 'user'
CHECK (participant_type IN ('user', 'persona', 'session')),
participant_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member'
CHECK (role IN ('owner', 'member', 'observer')),
display_name TEXT,
avatar_url TEXT,
joined_at TEXT DEFAULT (datetime('now')),
last_read_at TEXT DEFAULT (datetime('now')),
last_read_message_id TEXT,
UNIQUE(channel_id, participant_type, participant_id)
);
CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_id);
CREATE TABLE IF NOT EXISTS channel_models (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
display_name TEXT,
system_prompt TEXT,
settings TEXT DEFAULT '{}',
is_default INTEGER DEFAULT 0,
added_at TEXT DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw
ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona
ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
CREATE TABLE IF NOT EXISTS channel_cursors (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
active_leaf_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
updated_at TEXT DEFAULT (datetime('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);
CREATE TABLE IF NOT EXISTS user_model_settings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
hidden INTEGER DEFAULT 0,
preferred_temperature REAL,
preferred_max_tokens INTEGER,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(user_id, model_id, provider_config_id)
);
CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id);
CREATE TABLE IF NOT EXISTS session_participants (
id TEXT PRIMARY KEY,
session_token TEXT NOT NULL UNIQUE,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
display_name TEXT NOT NULL DEFAULT 'Visitor',
fingerprint TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_session_participants_channel ON session_participants(channel_id);
CREATE INDEX IF NOT EXISTS idx_session_participants_token ON session_participants(session_token);

View File

@@ -1,4 +1,6 @@
-- Chat Switchboard — 011 Notifications (SQLite)
-- ==========================================
-- Switchboard Core — 005 Notifications (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
@@ -12,8 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications (
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at);
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread
ON notifications(user_id, is_read, created_at);
CREATE INDEX IF NOT EXISTS idx_notifications_user_created
ON notifications(user_id, created_at);
CREATE TABLE IF NOT EXISTS notification_preferences (
id TEXT PRIMARY KEY,

View File

@@ -0,0 +1,20 @@
-- ==========================================
-- Switchboard Core — 006 Audit (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
metadata TEXT DEFAULT '{}',
ip_address TEXT,
user_agent TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id);
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);

View File

@@ -1,73 +0,0 @@
-- Chat Switchboard — 006 Knowledge Bases (SQLite)
CREATE TABLE IF NOT EXISTS knowledge_bases (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT 'global',
owner_id TEXT REFERENCES users(id) ON DELETE CASCADE,
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
embedding_config TEXT NOT NULL DEFAULT '{}',
document_count INTEGER NOT NULL DEFAULT 0,
chunk_count INTEGER NOT NULL DEFAULT 0,
total_bytes INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
discoverable INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope);
CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id);
CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id);
CREATE TABLE IF NOT EXISTS kb_documents (
id TEXT PRIMARY KEY,
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
storage_key TEXT NOT NULL,
extracted_text TEXT,
chunk_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
error TEXT,
uploaded_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id);
CREATE TABLE IF NOT EXISTS kb_chunks (
id TEXT PRIMARY KEY,
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
document_id TEXT NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
token_count INTEGER NOT NULL DEFAULT 0,
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id);
CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id);
CREATE TABLE IF NOT EXISTS channel_knowledge_bases (
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
enabled INTEGER NOT NULL DEFAULT 1,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (channel_id, kb_id)
);
CREATE TABLE IF NOT EXISTS persona_knowledge_bases (
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
auto_search INTEGER NOT NULL DEFAULT 0,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (persona_id, kb_id)
);
CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id);
CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id);

View File

@@ -1,34 +0,0 @@
-- Chat Switchboard — 007 Notes & Links (SQLite)
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT DEFAULT '',
folder_path TEXT DEFAULT '/',
tags TEXT DEFAULT '[]',
metadata TEXT DEFAULT '{}',
source_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
source_message_id TEXT,
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path);
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at);
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id);
CREATE INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id);
CREATE TABLE IF NOT EXISTS note_links (
source_note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
target_note_id TEXT REFERENCES notes(id) ON DELETE CASCADE,
target_title TEXT NOT NULL,
display_text TEXT,
is_transclusion INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (source_note_id, target_title)
);
CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id);

View File

@@ -1,5 +1,6 @@
-- Chat Switchboard — 018 Workflows (SQLite)
-- Consolidated v0.28.0: merges 023 + 025 + 027
-- ==========================================
-- Switchboard Core — 007 Workflows (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS workflows (
id TEXT PRIMARY KEY,
@@ -22,25 +23,28 @@ CREATE TABLE IF NOT EXISTS workflows (
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
ON workflows(slug) WHERE team_id IS NULL;
ON workflows(team_id, slug);
CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active);
CREATE TRIGGER IF NOT EXISTS workflows_updated_at AFTER UPDATE ON workflows
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE workflows SET updated_at = datetime('now') WHERE id = NEW.id; END;
CREATE TABLE IF NOT EXISTS workflow_stages (
id TEXT PRIMARY KEY,
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
ordinal INTEGER NOT NULL,
name TEXT NOT NULL,
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
persona_id TEXT,
assignment_team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
form_template TEXT NOT NULL DEFAULT '{}',
stage_mode TEXT NOT NULL DEFAULT 'chat_only'
CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')),
stage_mode TEXT NOT NULL DEFAULT 'form_only'
CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')),
history_mode TEXT NOT NULL DEFAULT 'full'
CHECK (history_mode IN ('full', 'summary', 'fresh')),
auto_transition INTEGER NOT NULL DEFAULT 0,
transition_rules TEXT NOT NULL DEFAULT '{}',
surface_pkg_id TEXT,
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
sla_seconds INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -59,22 +63,3 @@ CREATE TABLE IF NOT EXISTS workflow_versions (
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
ON workflow_versions(workflow_id, version_number);
CREATE TABLE IF NOT EXISTS workflow_assignments (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
stage INTEGER NOT NULL,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
assigned_to TEXT REFERENCES users(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'unassigned'
CHECK (status IN ('unassigned', 'claimed', 'completed')),
review_comments TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
claimed_at TEXT,
completed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel
ON workflow_assignments(channel_id, stage);
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status
ON workflow_assignments(team_id, status);

View File

@@ -1,45 +0,0 @@
-- Chat Switchboard — 008 Memory (SQLite)
-- Consolidated v0.28.0: adds last_compacted_at + decay_rate
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')),
owner_id TEXT NOT NULL,
user_id TEXT,
key TEXT NOT NULL,
value TEXT NOT NULL,
source_channel_id TEXT,
confidence REAL NOT NULL DEFAULT 1.0,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'archived')),
embedding TEXT,
last_compacted_at TEXT,
decay_rate REAL NOT NULL DEFAULT 0.0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_memories_scope_owner
ON memories(scope, owner_id, status);
CREATE INDEX IF NOT EXISTS idx_memories_persona_user
ON memories(owner_id, user_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key);
CREATE INDEX IF NOT EXISTS idx_memories_source_channel
ON memories(source_channel_id);
CREATE TABLE IF NOT EXISTS memory_extraction_log (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL,
user_id TEXT NOT NULL,
last_message_id TEXT NOT NULL,
extracted_at TEXT DEFAULT (datetime('now')),
memory_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(channel_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel
ON memory_extraction_log(channel_id);

View File

@@ -1,6 +1,6 @@
-- Chat Switchboard — 019 Tasks (SQLite)
-- Consolidated v0.28.0: merges 026 + 027
-- v0.28.0-cs3: trigger_token, action task_type, queued run status.
-- ==========================================
-- Switchboard Core — 008 Tasks (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
@@ -8,10 +8,12 @@ CREATE TABLE IF NOT EXISTS tasks (
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
name TEXT NOT NULL,
description TEXT DEFAULT '',
scope TEXT NOT NULL DEFAULT 'personal',
task_type TEXT NOT NULL DEFAULT 'prompt',
scope TEXT NOT NULL DEFAULT 'personal'
CHECK (scope IN ('personal', 'team', 'global')),
task_type TEXT NOT NULL DEFAULT 'action'
CHECK (task_type IN ('action', 'workflow', 'system')),
system_function TEXT DEFAULT '',
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
persona_id TEXT,
model_id TEXT,
system_prompt TEXT DEFAULT '',
user_prompt TEXT DEFAULT '',
@@ -24,11 +26,10 @@ CREATE TABLE IF NOT EXISTS tasks (
max_tokens INTEGER NOT NULL DEFAULT 4096,
max_tool_calls INTEGER NOT NULL DEFAULT 10,
max_wall_clock INTEGER NOT NULL DEFAULT 300,
output_mode TEXT NOT NULL DEFAULT 'channel',
output_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
output_mode TEXT NOT NULL DEFAULT 'log'
CHECK (output_mode IN ('notification', 'webhook', 'log')),
webhook_url TEXT,
webhook_secret TEXT,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
notify_on_complete INTEGER NOT NULL DEFAULT 0,
notify_on_failure INTEGER NOT NULL DEFAULT 1,
last_run_at TEXT,
@@ -38,16 +39,16 @@ CREATE TABLE IF NOT EXISTS tasks (
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = 1;
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id);
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at);
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id);
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks(trigger_token);
CREATE TABLE IF NOT EXISTS task_runs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'running',
status TEXT NOT NULL DEFAULT 'running'
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')),
trigger_payload TEXT,
started_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT,
@@ -57,5 +58,5 @@ CREATE TABLE IF NOT EXISTS task_runs (
error TEXT
);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status);

View File

@@ -0,0 +1,21 @@
-- ==========================================
-- Switchboard Core — 009 Multi-Replica HA (SQLite)
-- ==========================================
CREATE TABLE IF NOT EXISTS ws_tickets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at);
CREATE TABLE IF NOT EXISTS rate_limit_counters (
key TEXT NOT NULL,
window_start TEXT NOT NULL,
tokens REAL NOT NULL DEFAULT 0,
PRIMARY KEY (key, window_start)
);
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
ON rate_limit_counters(window_start);

View File

@@ -1,69 +0,0 @@
-- Chat Switchboard — 009 Workspaces & Git (SQLite)
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
owner_type TEXT NOT NULL CHECK (owner_type IN ('user', 'project', 'channel', 'team')),
owner_id TEXT NOT NULL,
name TEXT NOT NULL,
root_path TEXT NOT NULL,
max_bytes INTEGER,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleting')),
indexing_enabled INTEGER NOT NULL DEFAULT 1,
git_remote_url TEXT,
git_branch TEXT,
git_credential_id TEXT,
git_last_sync TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id);
CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status);
CREATE TABLE IF NOT EXISTS workspace_files (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
path TEXT NOT NULL,
is_directory INTEGER NOT NULL DEFAULT 0,
content_type TEXT,
size_bytes INTEGER NOT NULL DEFAULT 0,
sha256 TEXT,
index_status TEXT DEFAULT 'pending' CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')),
chunk_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path ON workspace_files(workspace_id, path);
CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id);
CREATE INDEX IF NOT EXISTS idx_workspace_files_type ON workspace_files(workspace_id, content_type);
CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status ON workspace_files(workspace_id, index_status);
CREATE TABLE IF NOT EXISTS workspace_chunks (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
file_id TEXT NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
token_count INTEGER NOT NULL DEFAULT 0,
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id);
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id);
CREATE TABLE IF NOT EXISTS git_credentials (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT '',
auth_type TEXT NOT NULL DEFAULT 'https_pat',
encrypted_data BLOB NOT NULL DEFAULT '',
nonce BLOB NOT NULL DEFAULT '',
public_key TEXT NOT NULL DEFAULT '',
fingerprint TEXT NOT NULL DEFAULT '',
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id);

View File

@@ -1,62 +0,0 @@
-- Chat Switchboard — 010 Projects (SQLite)
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
color TEXT,
icon TEXT,
scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'team', 'global')),
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
is_archived INTEGER NOT NULL DEFAULT 0,
settings TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id);
CREATE INDEX IF NOT EXISTS idx_projects_team ON projects(team_id);
CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope);
CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id);
CREATE TABLE IF NOT EXISTS project_channels (
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
folder TEXT,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (project_id, channel_id),
UNIQUE (channel_id)
);
CREATE INDEX IF NOT EXISTS idx_project_channels_project ON project_channels(project_id);
CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id);
CREATE TABLE IF NOT EXISTS project_knowledge_bases (
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
auto_search INTEGER NOT NULL DEFAULT 0,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (project_id, kb_id)
);
CREATE INDEX IF NOT EXISTS idx_project_kb_project ON project_knowledge_bases(project_id);
CREATE INDEX IF NOT EXISTS idx_project_kb_kb ON project_knowledge_bases(kb_id);
CREATE TABLE IF NOT EXISTS project_notes (
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (project_id, note_id)
);
CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id);
CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id);
-- Cross-domain FK columns on channels (SQLite cannot ADD COLUMN with FK in all cases,
-- so these may already exist from initial channel creation — safe to skip on error)
-- ALTER TABLE channels ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
-- ALTER TABLE channels ADD COLUMN workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL;
-- Note: SQLite ADD COLUMN IF NOT EXISTS not supported; channels table created with these columns.

View File

@@ -1,3 +0,0 @@
-- Chat Switchboard — 012 (placeholder, SQLite)
-- Previously: extensions + extension_user_settings tables.
-- v0.28.7: Both tables removed. Unified package registry in 016.

View File

@@ -1,28 +0,0 @@
-- Chat Switchboard — 013 Files (SQLite)
-- Consolidated v0.28.0: clean install, no legacy attachments migration
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
channel_id TEXT REFERENCES channels(id) ON DELETE CASCADE,
message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
origin TEXT NOT NULL DEFAULT 'user_upload'
CHECK (origin IN ('user_upload', 'tool_output', 'system')),
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes INTEGER NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
display_hint TEXT NOT NULL DEFAULT 'download'
CHECK (display_hint IN ('inline', 'download', 'thumbnail')),
extracted_text TEXT,
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id);
CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id);
CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id);
CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id);
CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin);

View File

@@ -1,42 +0,0 @@
-- Chat Switchboard — 014 Audit & Usage (SQLite)
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
metadata TEXT DEFAULT '{}',
ip_address TEXT,
user_agent TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id);
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);
CREATE TABLE IF NOT EXISTS usage_log (
id TEXT PRIMARY KEY,
channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
provider_scope TEXT NOT NULL DEFAULT 'global',
model_id TEXT NOT NULL,
role TEXT,
prompt_tokens INTEGER NOT NULL DEFAULT 0,
completion_tokens INTEGER NOT NULL DEFAULT 0,
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
cost_input REAL,
cost_output REAL,
routing_decision TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at);
CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope);

View File

@@ -1,17 +0,0 @@
-- Chat Switchboard — 015 Tool Health (SQLite)
CREATE TABLE IF NOT EXISTS tool_health (
id TEXT PRIMARY KEY,
tool_name TEXT NOT NULL,
window_start TEXT NOT NULL,
request_count INTEGER NOT NULL DEFAULT 0,
error_count INTEGER NOT NULL DEFAULT 0,
total_latency_ms INTEGER NOT NULL DEFAULT 0,
max_latency_ms INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
last_error_at TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tool_name, window_start)
);
CREATE INDEX IF NOT EXISTS idx_tool_health_window ON tool_health(tool_name, window_start);

View File

@@ -1,11 +0,0 @@
-- Chat Switchboard — 017 Auth Providers (SQLite)
-- Consolidated v0.28.0
CREATE TABLE IF NOT EXISTS oidc_auth_state (
state TEXT PRIMARY KEY,
nonce TEXT NOT NULL,
redirect_to TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);

View File

@@ -1,34 +0,0 @@
-- ==========================================
-- Chat Switchboard — 020 Multi-Replica HA
-- ==========================================
-- Shared state tables for multi-replica operation.
-- v0.32.0: ws_tickets, rate_limit_counters.
-- SQLite parity — functional for single-process test coverage.
-- ==========================================
-- =========================================
-- WEBSOCKET TICKETS
-- =========================================
CREATE TABLE IF NOT EXISTS ws_tickets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires
ON ws_tickets (expires_at);
-- =========================================
-- RATE LIMIT COUNTERS
-- =========================================
CREATE TABLE IF NOT EXISTS rate_limit_counters (
key TEXT NOT NULL,
window_start TEXT NOT NULL,
tokens REAL NOT NULL DEFAULT 0,
PRIMARY KEY (key, window_start)
);
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
ON rate_limit_counters (window_start);

View File

@@ -1,15 +0,0 @@
-- ==========================================
-- Chat Switchboard — 021 Data Portability
-- ==========================================
-- v0.34.0: Indexes for data export and GDPR operations.
-- SQLite version (no partial indexes).
-- ==========================================
CREATE INDEX IF NOT EXISTS idx_messages_channel_active
ON messages (channel_id, created_at);
CREATE INDEX IF NOT EXISTS idx_channels_user_id
ON channels (user_id);
CREATE INDEX IF NOT EXISTS idx_files_user_id
ON files (user_id);

View File

@@ -1,24 +0,0 @@
-- ==========================================
-- Chat Switchboard — 022 Extension Connections
-- ==========================================
-- v0.38.1: Scoped credential management for extensions.
-- SQLite version (no partial indexes, TEXT types).
-- ==========================================
CREATE TABLE IF NOT EXISTS ext_connections (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
package_id TEXT NOT NULL,
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
owner_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}',
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT,
UNIQUE(type, scope, owner_id, name)
);
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id);

View File

@@ -1,62 +0,0 @@
-- ==========================================
-- Chat Switchboard — 023 Extension Dependencies
-- ==========================================
-- v0.38.2: Library package dependency tracking.
-- SQLite version.
--
-- Also adds 'library' to the packages type CHECK constraint.
-- SQLite doesn't support ALTER CONSTRAINT, so we rebuild the table.
-- ==========================================
-- Step 1: Rebuild packages table with updated CHECK constraint.
CREATE TABLE IF NOT EXISTS packages_new (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'surface'
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
version TEXT NOT NULL DEFAULT '0.0.0',
description TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
tier TEXT NOT NULL DEFAULT 'browser'
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
is_system INTEGER NOT NULL DEFAULT 0,
scope TEXT NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team', 'personal')),
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
installed_by TEXT REFERENCES users(id) ON DELETE SET NULL,
manifest TEXT NOT NULL DEFAULT '{}',
enabled INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'suspended')),
schema_version INTEGER NOT NULL DEFAULT 0,
package_settings TEXT NOT NULL DEFAULT '{}',
source TEXT NOT NULL DEFAULT 'core'
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
installed_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT INTO packages_new SELECT * FROM packages;
DROP TABLE packages;
ALTER TABLE packages_new RENAME TO packages;
-- Recreate indexes (dropped with old table)
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled);
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
-- Step 2: Dependencies table.
CREATE TABLE IF NOT EXISTS ext_dependencies (
consumer_id TEXT NOT NULL,
library_id TEXT NOT NULL,
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
PRIMARY KEY (consumer_id, library_id),
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES packages(id)
);
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);