-- ============================================ -- TEMPLATE-SAAS: Feature Flags -- Schema: feature_flags -- Version: 1.0.0 -- ============================================ -- Feature flag definitions (global) CREATE TABLE feature_flags.flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Flag identification code VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, category VARCHAR(100), -- 'billing', 'ui', 'api', 'experimental' -- Status status feature_flags.flag_status DEFAULT 'disabled' NOT NULL, -- Rollout rollout_stage feature_flags.rollout_stage DEFAULT 'development', rollout_percentage INT DEFAULT 0, -- 0-100 -- Default value default_value BOOLEAN DEFAULT FALSE, -- Targeting rules (JSONB) targeting_rules JSONB DEFAULT '[]'::jsonb, -- Example: -- [ -- { "type": "plan", "operator": "in", "values": ["pro", "enterprise"] }, -- { "type": "user_attribute", "attribute": "country", "operator": "eq", "value": "MX" } -- ] -- Metadata metadata JSONB DEFAULT '{}'::jsonb, tags JSONB DEFAULT '[]'::jsonb, -- Lifecycle is_permanent BOOLEAN DEFAULT FALSE, -- Won't be cleaned up expires_at TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, created_by VARCHAR(100), -- Constraints CONSTRAINT valid_flag_code CHECK (code ~ '^[a-z][a-z0-9_]*$'), CONSTRAINT valid_percentage CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100) ); -- Tenant-specific flag overrides CREATE TABLE feature_flags.tenant_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE, -- Override value is_enabled BOOLEAN NOT NULL, -- Reason reason VARCHAR(500), -- Audit created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_by UUID, -- Constraints CONSTRAINT unique_tenant_flag UNIQUE (tenant_id, flag_id) ); -- User-specific flag overrides CREATE TABLE feature_flags.user_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE, -- Override value is_enabled BOOLEAN NOT NULL, -- Reason reason VARCHAR(500), -- Expiration expires_at TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_by UUID, -- Constraints CONSTRAINT unique_user_flag UNIQUE (user_id, flag_id) ); -- Flag evaluation history (for analytics) CREATE TABLE feature_flags.evaluations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE, user_id UUID REFERENCES users.users(id), -- Result flag_code VARCHAR(100) NOT NULL, result BOOLEAN NOT NULL, evaluation_reason VARCHAR(100), -- 'default', 'tenant_override', 'user_override', 'targeting_rule', 'percentage' -- Context context JSONB DEFAULT '{}'::jsonb, -- Timestamp created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); -- Indexes CREATE INDEX idx_flags_code ON feature_flags.flags(code); CREATE INDEX idx_flags_status ON feature_flags.flags(status) WHERE status != 'disabled'; CREATE INDEX idx_flags_category ON feature_flags.flags(category); CREATE INDEX idx_tenant_flags_tenant ON feature_flags.tenant_flags(tenant_id); CREATE INDEX idx_tenant_flags_flag ON feature_flags.tenant_flags(flag_id); CREATE INDEX idx_user_flags_user ON feature_flags.user_flags(user_id); CREATE INDEX idx_user_flags_tenant ON feature_flags.user_flags(tenant_id); CREATE INDEX idx_evaluations_tenant ON feature_flags.evaluations(tenant_id, created_at DESC); CREATE INDEX idx_evaluations_flag ON feature_flags.evaluations(flag_id, created_at DESC); -- RLS ALTER TABLE feature_flags.tenant_flags ENABLE ROW LEVEL SECURITY; ALTER TABLE feature_flags.user_flags ENABLE ROW LEVEL SECURITY; ALTER TABLE feature_flags.evaluations ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_flags_isolation ON feature_flags.tenant_flags USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); CREATE POLICY user_flags_isolation ON feature_flags.user_flags USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); CREATE POLICY evaluations_isolation ON feature_flags.evaluations USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Trigger CREATE OR REPLACE FUNCTION feature_flags.update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_flags_updated_at BEFORE UPDATE ON feature_flags.flags FOR EACH ROW EXECUTE FUNCTION feature_flags.update_updated_at(); CREATE TRIGGER trg_tenant_flags_updated_at BEFORE UPDATE ON feature_flags.tenant_flags FOR EACH ROW EXECUTE FUNCTION feature_flags.update_updated_at(); CREATE TRIGGER trg_user_flags_updated_at BEFORE UPDATE ON feature_flags.user_flags FOR EACH ROW EXECUTE FUNCTION feature_flags.update_updated_at(); -- Function to evaluate flag CREATE OR REPLACE FUNCTION feature_flags.evaluate_flag( p_flag_code VARCHAR, p_tenant_id UUID, p_user_id UUID DEFAULT NULL ) RETURNS BOOLEAN AS $$ DECLARE v_flag RECORD; v_tenant_override RECORD; v_user_override RECORD; v_result BOOLEAN; BEGIN -- Get flag SELECT * INTO v_flag FROM feature_flags.flags WHERE code = p_flag_code; IF NOT FOUND THEN RETURN FALSE; END IF; -- Check user override IF p_user_id IS NOT NULL THEN SELECT * INTO v_user_override FROM feature_flags.user_flags WHERE flag_id = v_flag.id AND user_id = p_user_id AND (expires_at IS NULL OR expires_at > NOW()); IF FOUND THEN RETURN v_user_override.is_enabled; END IF; END IF; -- Check tenant override SELECT * INTO v_tenant_override FROM feature_flags.tenant_flags WHERE flag_id = v_flag.id AND tenant_id = p_tenant_id; IF FOUND THEN RETURN v_tenant_override.is_enabled; END IF; -- Check status IF v_flag.status = 'disabled' THEN RETURN FALSE; ELSIF v_flag.status = 'enabled' THEN RETURN TRUE; ELSIF v_flag.status = 'percentage' THEN -- Simple percentage rollout based on tenant_id RETURN (abs(hashtext(p_tenant_id::TEXT)) % 100) < v_flag.rollout_percentage; END IF; RETURN v_flag.default_value; END; $$ LANGUAGE plpgsql STABLE; -- Comments COMMENT ON TABLE feature_flags.flags IS 'Global feature flag definitions'; COMMENT ON TABLE feature_flags.tenant_flags IS 'Tenant-specific flag overrides'; COMMENT ON TABLE feature_flags.user_flags IS 'User-specific flag overrides'; COMMENT ON TABLE feature_flags.evaluations IS 'Flag evaluation history for analytics'; COMMENT ON COLUMN feature_flags.flags.targeting_rules IS 'JSON rules for advanced targeting';