diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb
index d727ab2e625..138b124bfd8 100644
--- a/lib/discourse_plugin_registry.rb
+++ b/lib/discourse_plugin_registry.rb
@@ -246,8 +246,39 @@ class DiscoursePluginRegistry
     asset
   end
 
+  def self.clear_modifiers!
+    @modifiers = nil
+  end
+
+  def self.register_modifier(plugin_instance, name, &blk)
+    @modifiers ||= {}
+    modifiers = @modifiers[name] ||= []
+    modifiers << [plugin_instance, blk]
+  end
+
+  def self.apply_modifier(name, arg, *more_args)
+    return arg if !@modifiers
+
+    registered_modifiers = @modifiers[name]
+    return arg if !registered_modifiers
+
+    # iterate as fast as possible to minimize cost (avoiding each)
+    # also erases one stack frame
+    length = registered_modifiers.length
+    index = 0
+    while index < length
+      plugin_instance, block = registered_modifiers[index]
+      arg = block.call(arg, *more_args) if plugin_instance.enabled?
+
+      index += 1
+    end
+
+    arg
+  end
+
   def self.reset!
     @@register_names.each { |name| instance_variable_set(:"@#{name}", nil) }
+    clear_modifiers!
   end
 
   def self.reset_register!(register_name)
diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb
index ce8ce5e8b11..60eeb014e06 100644
--- a/lib/plugin/instance.rb
+++ b/lib/plugin/instance.rb
@@ -134,6 +134,10 @@ class Plugin::Instance
     end
   end
 
+  def register_modifier(modifier_name, &blk)
+    DiscoursePluginRegistry.register_modifier(self, modifier_name, &blk)
+  end
+
   # Applies to all sites in a multisite environment. Ignores plugin.enabled?
   def add_report(name, &block)
     reloadable_patch { |plugin| Report.add_report(name, &block) }
diff --git a/spec/lib/discourse_plugin_registry_spec.rb b/spec/lib/discourse_plugin_registry_spec.rb
index c74db5e14fc..b37bbd2bf6b 100644
--- a/spec/lib/discourse_plugin_registry_spec.rb
+++ b/spec/lib/discourse_plugin_registry_spec.rb
@@ -258,4 +258,55 @@ RSpec.describe DiscoursePluginRegistry do
       )
     end
   end
+
+  context "with filters" do
+    after { DiscoursePluginRegistry.clear_modifiers! }
+
+    class TestFilterPlugInstance < Plugin::Instance
+      def enabled?
+        !@disabled
+      end
+
+      def enabled=(value)
+        @disabled = !value
+      end
+    end
+
+    let(:plugin_instance) { TestFilterPlugInstance.new }
+    let(:plugin_instance2) { TestFilterPlugInstance.new }
+
+    it "handles modifiers with multiple parameters" do
+      DiscoursePluginRegistry.register_modifier(plugin_instance, :magic_sum_modifier) do |a, b|
+        a + b
+      end
+
+      sum = DiscoursePluginRegistry.apply_modifier(:magic_sum_modifier, 1, 2)
+      expect(sum).to eq(3)
+    end
+
+    it "handles modifier stacking" do
+      # first in, first called
+      DiscoursePluginRegistry.register_modifier(plugin_instance, :stacking) { |x| x == 1 ? 2 : 1 }
+      DiscoursePluginRegistry.register_modifier(plugin_instance2, :stacking) { |x| x + 1 }
+
+      expect(DiscoursePluginRegistry.apply_modifier(:stacking, 1)).to eq(3)
+    end
+
+    it "handles disabled plugins" do
+      plugin_instance.enabled = false
+      DiscoursePluginRegistry.register_modifier(plugin_instance, :magic_sum_modifier) do |a, b|
+        a + b
+      end
+
+      sum = DiscoursePluginRegistry.apply_modifier(:magic_sum_modifier, 1, 2)
+      expect(sum).to eq(1)
+    end
+
+    it "can handle arity mismatch" do
+      DiscoursePluginRegistry.register_modifier(plugin_instance, :magic_sum_modifier) { 42 }
+
+      sum = DiscoursePluginRegistry.apply_modifier(:magic_sum_modifier, 1, 2)
+      expect(sum).to eq(42)
+    end
+  end
 end
diff --git a/spec/lib/plugin/instance_spec.rb b/spec/lib/plugin/instance_spec.rb
index 4f69c994f39..750226c8acf 100644
--- a/spec/lib/plugin/instance_spec.rb
+++ b/spec/lib/plugin/instance_spec.rb
@@ -827,4 +827,17 @@ RSpec.describe Plugin::Instance do
       expect(callback_called).to eq(false)
     end
   end
+
+  describe "#register_modifier" do
+    let(:plugin) { Plugin::Instance.new }
+
+    after { DiscoursePluginRegistry.clear_modifiers! }
+
+    it "allows modifier registration" do
+      plugin.register_modifier(:magic_sum_modifier) { |a, b| a + b }
+
+      sum = DiscoursePluginRegistry.apply_modifier(:magic_sum_modifier, 1, 2)
+      expect(sum).to eq(3)
+    end
+  end
 end