-- ========================================== -- Migration 016: Teams -- ========================================== -- Teams are the middle tier between system admin and individual users. -- A team admin can manage members, create team-scoped presets, -- and enforce provider policies — without system-wide access. -- ========================================== -- ── 1. Teams table ────────────────────────── CREATE TABLE IF NOT EXISTS teams ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(200) NOT NULL UNIQUE, description TEXT DEFAULT '', created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, is_active BOOLEAN DEFAULT true, settings JSONB DEFAULT '{}'::jsonb, -- team-level policies created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = true; CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE teams IS 'Organizational teams with scoped administration'; COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}'; -- ── 2. Team Members table ─────────────────── CREATE TABLE IF NOT EXISTS team_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(20) NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')), joined_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(team_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id); CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id); COMMENT ON TABLE team_members IS 'Team membership with per-team roles (admin or member)'; COMMENT ON COLUMN team_members.role IS 'admin = manages team; member = uses team resources'; -- ── 3. Wire model_presets.team_id FK ──────── -- Column already exists (migration 012), just add the FK constraint. DO $$ BEGIN ALTER TABLE model_presets ADD CONSTRAINT fk_presets_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL; EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ── 4. Add team_id to channels ────────────── -- Team channels are visible to all team members. ALTER TABLE channels ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL; -- ── 5. Private provider flag ──────────────── -- Marks a provider as local/self-hosted (data stays on-prem). ALTER TABLE api_configs ADD COLUMN IF NOT EXISTS is_private BOOLEAN DEFAULT false; COMMENT ON COLUMN api_configs.is_private IS 'Private/self-hosted provider — data does not leave network'; -- ── 6. Add team_id to notes ───────────────── -- Team notes are shared within the team. ALTER TABLE notes ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;