diff --git a/app/finders/work_items/work_items_finder.rb b/app/finders/work_items/work_items_finder.rb
index 62cca06bf5e797ee4dd2323ac363fc032d631e64..07010adcf0d9204ee88cde20bf1d3a73cdcb38ff 100644
--- a/app/finders/work_items/work_items_finder.rb
+++ b/app/finders/work_items/work_items_finder.rb
@@ -19,7 +19,7 @@ def filter_items(items)
     end
 
     def by_widgets(items)
-      WorkItems::Type.available_widgets.each do |widget_class|
+      WorkItems::WidgetDefinition.available_widgets.each do |widget_class|
         widget_filter = widget_filter_for(widget_class)
 
         next unless widget_filter
diff --git a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
index 508e1627032fde81fc19d10a22c14bebd3cc87a1..3f32cd51ae76b5529714048a92816e0d289732b1 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
@@ -7,7 +7,7 @@ module Widgetable
 
       def extract_widget_params!(work_item_type, attributes)
         # Get the list of widgets for the work item's type to extract only the supported attributes
-        widget_keys = ::WorkItems::Type.available_widgets.map(&:api_symbol)
+        widget_keys = ::WorkItems::WidgetDefinition.available_widgets.map(&:api_symbol)
         widget_params = attributes.extract!(*widget_keys)
 
         not_supported_keys = widget_params.keys - work_item_type.widgets.map(&:api_symbol)
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 42ec41d9d8d8e3899fb7f12b75492a5650af40f2..0c9aac802749a1399cc9b680c6877269e890c756 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -43,7 +43,7 @@ def preloads
       {
         work_item_type: :work_item_type,
         web_url: { project: { namespace: :route } },
-        widgets: :work_item_type
+        widgets: { work_item_type: :enabled_widget_definitions }
       }
     end
 
diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb
index 4e5933bff869b7aab175a3102fa62d7894d7f83a..2ad951d421b4631ed88cf75b034d598540f16004 100644
--- a/app/graphql/types/work_items/widget_type_enum.rb
+++ b/app/graphql/types/work_items/widget_type_enum.rb
@@ -6,8 +6,8 @@ class WidgetTypeEnum < BaseEnum
       graphql_name 'WorkItemWidgetType'
       description 'Type of a work item widget'
 
-      ::WorkItems::Type.available_widgets.each do |widget|
-        value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget."
+      ::WorkItems::WidgetDefinition.widget_classes.each do |cls|
+        value cls.type.to_s.upcase, value: cls.type, description: "#{cls.type.to_s.titleize} widget."
       end
     end
   end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 258a86d73164bc33ffcb01b725d714f1e4210c02..6a619dbab21b0ce2158434d5b54f12e1b81fdb73 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -35,56 +35,6 @@ class Type < ApplicationRecord
       key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
     }.freeze
 
-    WIDGETS_FOR_TYPE = {
-      issue: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      incident: [
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::Notes
-      ],
-      test_case: [
-        Widgets::Description,
-        Widgets::Notes
-      ],
-      requirement: [
-        Widgets::Description,
-        Widgets::Notes
-      ],
-      task: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      objective: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      key_result: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Notes
-      ]
-    }.freeze
-
     # A list of types user can change between - both original and new
     # type must be included in this list. This is needed for legacy issues
     # where it's possible to switch between issue and incident.
@@ -98,6 +48,9 @@ class Type < ApplicationRecord
 
     belongs_to :namespace, optional: true
     has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
+    has_many :widget_definitions, foreign_key: :work_item_type_id, inverse_of: :work_item_type
+    has_many :enabled_widget_definitions, -> { where(disabled: false) }, foreign_key: :work_item_type_id,
+      inverse_of: :work_item_type, class_name: 'WorkItems::WidgetDefinition'
 
     before_validation :strip_whitespace
 
@@ -112,10 +65,6 @@ class Type < ApplicationRecord
     scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) }
     scope :by_type, ->(base_type) { where(base_type: base_type) }
 
-    def self.available_widgets
-      WIDGETS_FOR_TYPE.values.flatten.uniq
-    end
-
     def self.default_by_type(type)
       found_type = find_by(namespace_id: nil, base_type: type)
       return found_type if found_type
@@ -138,7 +87,7 @@ def default?
     end
 
     def widgets
-      WIDGETS_FOR_TYPE[base_type.to_sym]
+      enabled_widget_definitions.filter_map(&:widget_class)
     end
 
     def supports_assignee?
@@ -156,5 +105,3 @@ def strip_whitespace
     end
   end
 end
-
-WorkItems::Type.prepend_mod
diff --git a/app/models/work_items/widget_definition.rb b/app/models/work_items/widget_definition.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d4414e95d8a663877f485972382160bc1586b6b
--- /dev/null
+++ b/app/models/work_items/widget_definition.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module WorkItems
+  class WidgetDefinition < ApplicationRecord
+    self.table_name = 'work_item_widget_definitions'
+
+    belongs_to :namespace, optional: true
+    belongs_to :work_item_type, class_name: 'WorkItems::Type', inverse_of: :widget_definitions
+
+    validates :name, presence: true
+    validates :name, uniqueness: { case_sensitive: false, scope: [:namespace_id, :work_item_type_id] }
+    validates :name, length: { maximum: 255 }
+
+    scope :enabled, -> { where(disabled: false) }
+    scope :global, -> { where(namespace: nil) }
+
+    enum widget_type: {
+      assignees: 0,
+      description: 1,
+      hierarchy: 2,
+      labels: 3,
+      milestone: 4,
+      notes: 5,
+      start_and_due_date: 6,
+      health_status: 7, # EE-only
+      weight: 8, # EE-only
+      iteration: 9, # EE-only
+      progress: 10, # EE-only
+      status: 11, # EE-only
+      requirement_legacy: 12, # EE-only
+      test_reports: 13 # EE-only
+    }
+
+    def self.available_widgets
+      global.enabled.filter_map(&:widget_class).uniq
+    end
+
+    def self.widget_classes
+      WorkItems::WidgetDefinition.widget_types.keys.filter_map do |type|
+        WorkItems::Widgets.const_get(type.camelize, false)
+      rescue NameError
+        nil
+      end
+    end
+
+    def widget_class
+      return unless widget_type
+
+      WorkItems::Widgets.const_get(widget_type.camelize, false)
+    rescue NameError
+      nil
+    end
+  end
+end
diff --git a/db/docs/work_item_types.yml b/db/docs/work_item_types.yml
index 21ec69da15256621509f03c4554e09243f97da7e..37d2c47de252cb101c4e850f37a86abbca46e4c5 100644
--- a/db/docs/work_item_types.yml
+++ b/db/docs/work_item_types.yml
@@ -1,10 +1,12 @@
 ---
 table_name: work_item_types
 classes:
+- AddWidgetsForWorkItemTypes::WorkItemType
 - WorkItems::Type
 feature_categories:
 - team_planning
-description: The work item type related to an issue. Currently one of a predefined set but in future will support custom types.
+description: The work item type related to an issue. Currently one of a predefined
+  set but in future will support custom types.
 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55705
 milestone: '14.2'
 gitlab_schema: gitlab_main
diff --git a/db/docs/work_item_widget_definitions.yml b/db/docs/work_item_widget_definitions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59cbca14908952bba396d38d996b1bf5aab3dc3a
--- /dev/null
+++ b/db/docs/work_item_widget_definitions.yml
@@ -0,0 +1,12 @@
+---
+table_name: work_item_widget_definitions
+classes:
+- AddWidgetsForWorkItemTypes::WidgetDefinition
+- WorkItems::WidgetDefinition
+feature_categories:
+- team_planning
+description: Mapping of widgets for each work item type. Currently one of a predefined
+  set but in future will support custom types.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107582
+milestone: '15.9'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230129094140_add_widget_definitions.rb b/db/migrate/20230129094140_add_widget_definitions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09816f7386d26997427c0d69a57b6875439ff096
--- /dev/null
+++ b/db/migrate/20230129094140_add_widget_definitions.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddWidgetDefinitions < Gitlab::Database::Migration[2.1]
+  UNIQUE_INDEX_NAME = 'index_work_item_widget_definitions_on_namespace_type_and_name'
+  UNIQUE_DEFAULT_NAMESPACE_INDEX_NAME = 'index_work_item_widget_definitions_on_default_witype_and_name'
+
+  def up
+    create_table :work_item_widget_definitions do |t|
+      t.references :namespace, index: false
+      t.references :work_item_type, index: true, null: false
+      t.integer :widget_type, null: false, limit: 2
+      t.boolean :disabled, default: false
+      t.text :name, limit: 255
+
+      t.index [:namespace_id, :work_item_type_id, :name], unique: true, name: UNIQUE_INDEX_NAME
+      t.index [:work_item_type_id, :name], where: "namespace_id is NULL",
+        unique: true, name: UNIQUE_DEFAULT_NAMESPACE_INDEX_NAME
+    end
+  end
+
+  def down
+    drop_table :work_item_widget_definitions
+  end
+end
diff --git a/db/migrate/20230129154126_add_widget_def_namespace_fk.rb b/db/migrate/20230129154126_add_widget_def_namespace_fk.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf3f83fdbfef53f93e1d09d80a56654bb92daec5
--- /dev/null
+++ b/db/migrate/20230129154126_add_widget_def_namespace_fk.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddWidgetDefNamespaceFk < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :work_item_widget_definitions, :work_item_types,
+      column: :work_item_type_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :work_item_widget_definitions, column: :work_item_type_id
+    end
+  end
+end
diff --git a/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb b/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb
new file mode 100644
index 0000000000000000000000000000000000000000..530f2c78198ca02c85d518609cc741b20aa2e48e
--- /dev/null
+++ b/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddWidgetDefWorkItemTypeFk < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :work_item_widget_definitions, :namespaces, column: :namespace_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :work_item_widget_definitions, column: :namespace_id
+    end
+  end
+end
diff --git a/db/migrate/20230129154819_add_widgets_for_work_item_types.rb b/db/migrate/20230129154819_add_widgets_for_work_item_types.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b936ea2e409bdd37d37558e627fe66be97691738
--- /dev/null
+++ b/db/migrate/20230129154819_add_widgets_for_work_item_types.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+class AddWidgetsForWorkItemTypes < Gitlab::Database::Migration[2.1]
+  class WorkItemType < MigrationRecord
+    self.table_name = 'work_item_types'
+  end
+
+  class WidgetDefinition < MigrationRecord
+    self.table_name = 'work_item_widget_definitions'
+  end
+
+  restrict_gitlab_migration gitlab_schema: :gitlab_main
+  disable_ddl_transaction!
+
+  def up
+    widget_names = {
+      assignees: 'Assignees',
+      labels: 'Labels',
+      description: 'Description',
+      hierarchy: 'Hierarchy',
+      start_and_due_date: 'Start and due date',
+      milestone: 'Milestone',
+      notes: 'Notes',
+      iteration: 'Iteration',
+      weight: 'Weight',
+      health_status: 'Health status',
+      progress: 'Progress',
+      status: 'Status',
+      requirement_legacy: 'Requirement legacy',
+      test_reports: 'Test reports'
+    }
+
+    widgets_for_type = {
+      'Issue' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :milestone,
+        :notes,
+        # EE widgets
+        :iteration,
+        :weight,
+        :health_status
+      ],
+      'Incident' => [
+        :description,
+        :hierarchy,
+        :notes
+      ],
+      'Test Case' => [
+        :description,
+        :notes
+      ],
+      'Requirement' => [
+        :description,
+        :notes,
+        :status,
+        :requirement_legacy,
+        :test_reports
+      ],
+      'Task' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :milestone,
+        :notes,
+        :iteration,
+        :weight
+      ],
+      'Objective' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :milestone,
+        :notes,
+        :health_status,
+        :progress
+      ],
+      'Key Result' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :notes,
+        :health_status,
+        :progress
+      ]
+    }
+
+    widgets_enum = {
+      assignees: 0,
+      description: 1,
+      hierarchy: 2,
+      labels: 3,
+      milestone: 4,
+      notes: 5,
+      start_and_due_date: 6,
+      health_status: 7, # EE-only
+      weight: 8, # EE-only
+      iteration: 9, # EE-only
+      progress: 10, # EE-only
+      status: 11, # EE-only
+      requirement_legacy: 12, # EE-only
+      test_reports: 13
+    }
+
+    widgets = []
+    widgets_for_type.each do |type_name, widget_syms|
+      type = WorkItemType.find_by_name_and_namespace_id(type_name, nil)
+
+      unless type
+        Gitlab::AppLogger.warn("type #{type_name} is missing, not adding widgets")
+
+        next
+      end
+
+      widgets += widget_syms.map do |widget_sym|
+        {
+          work_item_type_id: type.id,
+          name: widget_names[widget_sym],
+          widget_type: widgets_enum[widget_sym]
+        }
+      end
+    end
+
+    return if widgets.empty?
+
+    WidgetDefinition.upsert_all(
+      widgets,
+      unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
+    )
+  end
+
+  def down
+    WidgetDefinition.delete_all
+  end
+end
diff --git a/db/schema_migrations/20230129094140 b/db/schema_migrations/20230129094140
new file mode 100644
index 0000000000000000000000000000000000000000..1e8543ebfd63299b4923136fb1fbb4fa91805be2
--- /dev/null
+++ b/db/schema_migrations/20230129094140
@@ -0,0 +1 @@
+e14187450e98a7ea699beecbc41733f6a524a1612cc0acbea3aa5b75a4f7be49
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154126 b/db/schema_migrations/20230129154126
new file mode 100644
index 0000000000000000000000000000000000000000..f7cecf227d98fa3ad1d4f6c8c51c35adf2bea81a
--- /dev/null
+++ b/db/schema_migrations/20230129154126
@@ -0,0 +1 @@
+685bc851446d875d72f5712533b13baea90f6f3bc82d383f1fff10859c341e49
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154202 b/db/schema_migrations/20230129154202
new file mode 100644
index 0000000000000000000000000000000000000000..6af55471b918f8a2e1428f16f12ed16a256e3d66
--- /dev/null
+++ b/db/schema_migrations/20230129154202
@@ -0,0 +1 @@
+a6146c49a1930b1cad7f56e6c3a8dbd433bd605d965a083f2dea3ab25261b94d
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154819 b/db/schema_migrations/20230129154819
new file mode 100644
index 0000000000000000000000000000000000000000..42bae9a9e20204adcfd61dc0627a8bd3d02ea894
--- /dev/null
+++ b/db/schema_migrations/20230129154819
@@ -0,0 +1 @@
+c8d2063f94253e79ff3c707e2af75a963863bcf601993c81e3043bbc5c2ae21b
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e9bdeb7201facc4598d1a16f8ae34707b605f671..2faa6fb4d575232284b179c926ef67ff18d0626c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23987,6 +23987,25 @@ CREATE SEQUENCE work_item_types_id_seq
 
 ALTER SEQUENCE work_item_types_id_seq OWNED BY work_item_types.id;
 
+CREATE TABLE work_item_widget_definitions (
+    id bigint NOT NULL,
+    namespace_id bigint,
+    work_item_type_id bigint NOT NULL,
+    widget_type smallint NOT NULL,
+    disabled boolean DEFAULT false,
+    name text,
+    CONSTRAINT check_050f2e2328 CHECK ((char_length(name) <= 255))
+);
+
+CREATE SEQUENCE work_item_widget_definitions_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE work_item_widget_definitions_id_seq OWNED BY work_item_widget_definitions.id;
+
 CREATE TABLE x509_certificates (
     id bigint NOT NULL,
     created_at timestamp with time zone NOT NULL,
@@ -25194,6 +25213,8 @@ ALTER TABLE ONLY work_item_parent_links ALTER COLUMN id SET DEFAULT nextval('wor
 
 ALTER TABLE ONLY work_item_types ALTER COLUMN id SET DEFAULT nextval('work_item_types_id_seq'::regclass);
 
+ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass);
+
 ALTER TABLE ONLY x509_certificates ALTER COLUMN id SET DEFAULT nextval('x509_certificates_id_seq'::regclass);
 
 ALTER TABLE ONLY x509_commit_signatures ALTER COLUMN id SET DEFAULT nextval('x509_commit_signatures_id_seq'::regclass);
@@ -27674,6 +27695,9 @@ ALTER TABLE ONLY work_item_progresses
 ALTER TABLE ONLY work_item_types
     ADD CONSTRAINT work_item_types_pkey PRIMARY KEY (id);
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT work_item_widget_definitions_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY x509_certificates
     ADD CONSTRAINT x509_certificates_pkey PRIMARY KEY (id);
 
@@ -32149,6 +32173,12 @@ CREATE UNIQUE INDEX index_work_item_parent_links_on_work_item_id ON work_item_pa
 
 CREATE INDEX index_work_item_parent_links_on_work_item_parent_id ON work_item_parent_links USING btree (work_item_parent_id);
 
+CREATE UNIQUE INDEX index_work_item_widget_definitions_on_default_witype_and_name ON work_item_widget_definitions USING btree (work_item_type_id, name) WHERE (namespace_id IS NULL);
+
+CREATE UNIQUE INDEX index_work_item_widget_definitions_on_namespace_type_and_name ON work_item_widget_definitions USING btree (namespace_id, work_item_type_id, name);
+
+CREATE INDEX index_work_item_widget_definitions_on_work_item_type_id ON work_item_widget_definitions USING btree (work_item_type_id);
+
 CREATE INDEX index_x509_certificates_on_subject_key_identifier ON x509_certificates USING btree (subject_key_identifier);
 
 CREATE INDEX index_x509_certificates_on_x509_issuer_id ON x509_certificates USING btree (x509_issuer_id);
@@ -34048,6 +34078,9 @@ ALTER TABLE ONLY user_achievements
 ALTER TABLE ONLY merge_requests
     ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT fk_61bfa96db5 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY deployment_approvals
     ADD CONSTRAINT fk_61cdbdc5b9 FOREIGN KEY (approval_rule_id) REFERENCES protected_environment_approval_rules(id) ON DELETE SET NULL;
 
@@ -34591,6 +34624,9 @@ ALTER TABLE ONLY pages_domains
 ALTER TABLE ONLY merge_requests_compliance_violations
     ADD CONSTRAINT fk_ec881c1c6f FOREIGN KEY (violating_user_id) REFERENCES users(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT fk_ecf57512f7 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY events
     ADD CONSTRAINT fk_edfd187b6f FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
 
diff --git a/doc/development/work_items_widgets.md b/doc/development/work_items_widgets.md
index ba15a3f0163d11c559cc12eae562711b22602c47..5b9602595bb0adaa0e0dd2ff430cd919fdcf6e52 100644
--- a/doc/development/work_items_widgets.md
+++ b/doc/development/work_items_widgets.md
@@ -112,3 +112,28 @@ for work items widgets development:
 
 - `ee/app/assets/javascripts/boards/components/assignee_select.vue`
 - `ee/app/assets/javascripts/boards/components/milestone_select.vue`
+
+## Mapping widgets to work item types
+
+All Work Item types share the same pool of predefined widgets and are customized by which widgets are active on a specific type. Because we plan to allow users to create new Work Item types and define a set of widgets for them, mapping of widgets for each Work Item type is stored in database. Mapping of widgets is stored in widget_definitions table and it can be used for defining widgets both for default Work Item types and also in future for custom types. More details about expected database table structure can be found in [this issue description](https://gitlab.com/gitlab-org/gitlab/-/issues/374092).
+
+### Adding new widget to a work item type
+
+Because information about what widgets are assigned to each work item type is stored in database, adding new widget to a work item type needs to be done through a database migration. Also widgets importer (`lib/gitlab/database_importers/work_items/widgets_importer.rb`) should be updated.
+
+### Structure of widget definitions table
+
+Each record in the table defines mapping of a widget to a work item type. Currently only "global" definitions (definitions with NULL namespace_id) are used. In next iterations we plan to allow customization of these mappings. For example table below defines that:
+
+- Weight widget is enabled for work item types 0 and 1
+- in namespace 1 Weight widget is renamed to MyWeight. When user renames widget's name, it makes sense to rename all widget mappings in the namespace - because `name` attribute is denormalized, we have to create namespaced mappings for all work item types for this widget type.
+- Weight widget can be disabled for specific work item types (in namespace 3 it's disabled for work item type 0, while still left enabled for work item type 1)
+
+| ID | Namespace_id | Work_item_type_id | Widget_type_enum | Position | Name      | Disabled |
+|----| ------------ | ----------------- |----------------- |--------- |---------- |-------|
+| 1  |              | 0                 | 1                | 1        | Weight    | false |
+| 2  |              | 1                 | 1                | 1        | Weight    | false |
+| 3  | 1            | 0                 | 1                | 0        | MyWeight  | false |
+| 4  | 1            | 1                 | 1                | 0        | MyWeight  | false |
+| 5  | 2            | 0                 | 1                | 1        | Other Weight | false |
+| 6  | 3            | 0                 | 1                | 1        | Weight | true |
diff --git a/ee/app/models/ee/work_items/type.rb b/ee/app/models/ee/work_items/type.rb
deleted file mode 100644
index 913e4ce4cc2018f9566ee201e6ea12f15dfeac8c..0000000000000000000000000000000000000000
--- a/ee/app/models/ee/work_items/type.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module WorkItems
-    module Type
-      extend ActiveSupport::Concern
-      extend ::Gitlab::Utils::Override
-
-      EE_WIDGETS_FOR_TYPE = {
-        issue: [
-          ::WorkItems::Widgets::Iteration,
-          ::WorkItems::Widgets::Weight,
-          ::WorkItems::Widgets::HealthStatus
-        ],
-        requirement: [
-          ::WorkItems::Widgets::Status,
-          ::WorkItems::Widgets::RequirementLegacy,
-          ::WorkItems::Widgets::TestReports
-        ],
-        task: [::WorkItems::Widgets::Iteration, ::WorkItems::Widgets::Weight],
-        objective: [::WorkItems::Widgets::HealthStatus, ::WorkItems::Widgets::Progress],
-        key_result: [::WorkItems::Widgets::HealthStatus, ::WorkItems::Widgets::Progress]
-      }.freeze
-
-      class_methods do
-        extend ::Gitlab::Utils::Override
-
-        override :available_widgets
-        def available_widgets
-          [*EE_WIDGETS_FOR_TYPE.values.flatten.uniq, *super]
-        end
-      end
-
-      override :widgets
-      def widgets
-        [*EE_WIDGETS_FOR_TYPE[base_type.to_sym], *super]
-      end
-    end
-  end
-end
diff --git a/ee/spec/models/ee/work_items/type_spec.rb b/ee/spec/models/ee/work_items/widget_definition_spec.rb
similarity index 91%
rename from ee/spec/models/ee/work_items/type_spec.rb
rename to ee/spec/models/ee/work_items/widget_definition_spec.rb
index c0ef26aba6f8e818527e451986fcb68b6ecd46be..ab6a1c3d612b527455f73b47d5e2ba26028395af 100644
--- a/ee/spec/models/ee/work_items/type_spec.rb
+++ b/ee/spec/models/ee/work_items/widget_definition_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe WorkItems::Type do
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
   describe '.available_widgets' do
     subject { described_class.available_widgets }
 
diff --git a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 550d88ef1e15f31905f6dd011eec149dc441237e..37d6d4ffa6ccad46fe879b27ad59578d06302152 100644
--- a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -237,7 +237,8 @@
             end
 
             before do
-              stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+              WorkItems::Type.default_by_type(:task).widget_definitions
+                .find_by_widget_type(:weight).update!(disabled: true)
             end
 
             it_behaves_like 'work item is not updated'
@@ -505,14 +506,15 @@ def work_item_status
           end
 
           context 'when the work item type does not support the health status widget' do
-            let_it_be(:work_item) { create(:work_item, :task, project: project) }
+            let_it_be(:work_item) { create(:work_item, :issue, project: project) }
 
             let(:input) do
               { 'descriptionWidget' => { 'description' => "Updating health status.\n/health_status on_track" } }
             end
 
             before do
-              stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+              WorkItems::Type.default_by_type(:issue).widget_definitions
+                .find_by_widget_type(:health_status).update!(disabled: true)
             end
 
             it_behaves_like 'work item is not updated'
diff --git a/ee/spec/services/work_items/update_service_spec.rb b/ee/spec/services/work_items/update_service_spec.rb
index 34db1b447ce3c6f8deeb377c9fe1ca0c5de5f291..87a2d4b109ac2a2baca69cf8851594ce3fc6aec7 100644
--- a/ee/spec/services/work_items/update_service_spec.rb
+++ b/ee/spec/services/work_items/update_service_spec.rb
@@ -6,7 +6,7 @@
   let_it_be(:developer) { create(:user) }
   let_it_be(:group) { create(:group) }
   let_it_be(:project) { create(:project, group: group).tap { |proj| proj.add_developer(developer) } }
-  let_it_be(:work_item) { create(:work_item, project: project) }
+  let_it_be_with_refind(:work_item) { create(:work_item, project: project) }
 
   let(:spam_params) { double }
   let(:current_user) { developer }
diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb
index 1e29ae7761bd18ae20e8dca050f51625db0a4da5..9796a5905e35c8db1cbfd1514e2ce39b9b824f21 100644
--- a/lib/gitlab/database_importers/work_items/base_type_importer.rb
+++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb
@@ -4,6 +4,85 @@ module Gitlab
   module DatabaseImporters
     module WorkItems
       module BaseTypeImporter
+        WIDGET_NAMES = {
+          assignees: 'Assignees',
+          labels: 'Labels',
+          description: 'Description',
+          hierarchy: 'Hierarchy',
+          start_and_due_date: 'Start and due date',
+          milestone: 'Milestone',
+          notes: 'Notes',
+          iteration: 'Iteration',
+          weight: 'Weight',
+          health_status: 'Health status',
+          progress: 'Progress',
+          status: 'Status',
+          requirement_legacy: 'Requirement legacy',
+          test_reports: 'Test reports'
+        }.freeze
+
+        WIDGETS_FOR_TYPE = {
+          issue: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :milestone,
+            :notes,
+            :iteration,
+            :weight,
+            :health_status
+          ],
+          incident: [
+            :description,
+            :hierarchy,
+            :notes
+          ],
+          test_case: [
+            :description,
+            :notes
+          ],
+          requirement: [
+            :description,
+            :notes,
+            :status,
+            :requirement_legacy,
+            :test_reports
+          ],
+          task: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :milestone,
+            :notes,
+            :iteration,
+            :weight
+          ],
+          objective: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :milestone,
+            :notes,
+            :health_status,
+            :progress
+          ],
+          key_result: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :notes,
+            :health_status,
+            :progress
+          ]
+        }.freeze
+
         def self.upsert_types
           current_time = Time.current
 
@@ -16,6 +95,29 @@ def self.upsert_types
             base_types,
             unique_by: :idx_work_item_types_on_namespace_id_and_name_null_namespace
           )
+
+          upsert_widgets
+        end
+
+        def self.upsert_widgets
+          type_ids_by_name = ::WorkItems::Type.default.pluck(:name, :id).to_h # rubocop: disable CodeReuse/ActiveRecord
+
+          widgets = WIDGETS_FOR_TYPE.flat_map do |type_sym, widget_syms|
+            type_name = ::WorkItems::Type::TYPE_NAMES[type_sym]
+
+            widget_syms.map do |widget_sym|
+              {
+                work_item_type_id: type_ids_by_name[type_name],
+                name: WIDGET_NAMES[widget_sym],
+                widget_type: ::WorkItems::WidgetDefinition.widget_types[widget_sym]
+              }
+            end
+          end
+
+          ::WorkItems::WidgetDefinition.upsert_all(
+            widgets,
+            unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
+          )
         end
       end
     end
diff --git a/spec/db/development/create_base_work_item_types_spec.rb b/spec/db/development/create_base_work_item_types_spec.rb
index 914b84d866888b2023f21a6de215ed3f1bc4a7fd..7652ccdc487da793574b8ed9cb55183259ebc436 100644
--- a/spec/db/development/create_base_work_item_types_spec.rb
+++ b/spec/db/development/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Create base work item types in development' do
+RSpec.describe 'Create base work item types in development', feature_category: :team_planning do
   subject { load Rails.root.join('db', 'fixtures', 'development', '001_create_base_work_item_types.rb') }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/db/production/create_base_work_item_types_spec.rb b/spec/db/production/create_base_work_item_types_spec.rb
index 81d80104bb4cc305ef2a4f3973513bb76fb69584..f6c3b0f6395ef4dfe20418c664e7af7553598a4b 100644
--- a/spec/db/production/create_base_work_item_types_spec.rb
+++ b/spec/db/production/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Create base work item types in production' do
+RSpec.describe 'Create base work item types in production', feature_category: :team_planning do
   subject { load Rails.root.join('db', 'fixtures', 'production', '003_create_base_work_item_types.rb') }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/factories/work_items/widget_definitions.rb b/spec/factories/work_items/widget_definitions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bbd7c1e7432650e1782be626e512b6d8a216445d
--- /dev/null
+++ b/spec/factories/work_items/widget_definitions.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :widget_definition, class: 'WorkItems::WidgetDefinition' do
+    work_item_type
+    namespace
+
+    name { 'Description' }
+    widget_type { 'description' }
+  end
+end
diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb
index 27c3b9643ce8d0830703ae6a9894bcb784f4283f..224213d1521a1374ec7b366fc183556cd6a339f7 100644
--- a/spec/graphql/resolvers/users/participants_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb
@@ -115,8 +115,8 @@
           create(:award_emoji, name: 'thumbsup', awardable: public_note)
 
           # 1 extra query per source (3 emojis + 2 notes) to fetch participables collection
-          # 1 extra query to load work item widgets collection
-          expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(6)
+          # 2 extra queries to load work item widgets collection
+          expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(7)
         end
 
         it 'does not execute N+1 for system note metadata relation' do
diff --git a/spec/graphql/resolvers/work_items_resolver_spec.rb b/spec/graphql/resolvers/work_items_resolver_spec.rb
index d89ccc7f8067ee20d9215823ea31e3fa90a10dc9..6da62e3adb7f353880ec0bd8801d9f51d4995d77 100644
--- a/spec/graphql/resolvers/work_items_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_items_resolver_spec.rb
@@ -101,7 +101,7 @@
       end
 
       it 'batches queries that only include IIDs', :request_store do
-        result = batch_sync(max_queries: 7) do
+        result = batch_sync(max_queries: 8) do
           [item1, item2]
             .map { |item| resolve_items(iid: item.iid.to_s) }
             .flat_map(&:to_a)
@@ -111,7 +111,7 @@
       end
 
       it 'finds a specific item with iids', :request_store do
-        result = batch_sync(max_queries: 7) do
+        result = batch_sync(max_queries: 8) do
           resolve_items(iids: [item1.iid]).to_a
         end
 
diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
index d044170dc75da224641d5d63e6e424d4ea28261f..3b6d10f4a7e46e0293b6edc48afd423c823ee055 100644
--- a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
+++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do
+RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter, feature_category: :team_planning do
   subject { described_class.upsert_types }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 65c6b22f5c243acb60338cc16487e3533e9af558..e5c88634b26369f7535ac158b45e9e39132c073f 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -10,6 +10,20 @@
   describe 'associations' do
     it { is_expected.to have_many(:work_items).with_foreign_key('work_item_type_id') }
     it { is_expected.to belong_to(:namespace) }
+
+    it 'has many `widget_definitions`' do
+      is_expected.to have_many(:widget_definitions)
+        .class_name('::WorkItems::WidgetDefinition')
+        .with_foreign_key('work_item_type_id')
+    end
+
+    it 'has many `enabled_widget_definitions`' do
+      type = create(:work_item_type)
+      widget1 = create(:widget_definition, work_item_type: type)
+      create(:widget_definition, work_item_type: type, disabled: true)
+
+      expect(type.enabled_widget_definitions).to match_array([widget1])
+    end
   end
 
   describe 'scopes' do
@@ -60,29 +74,14 @@
     it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
   end
 
-  describe '.available_widgets' do
-    subject { described_class.available_widgets }
-
-    it 'returns list of all possible widgets' do
-      is_expected.to include(
-        ::WorkItems::Widgets::Description,
-        ::WorkItems::Widgets::Hierarchy,
-        ::WorkItems::Widgets::Labels,
-        ::WorkItems::Widgets::Assignees,
-        ::WorkItems::Widgets::StartAndDueDate,
-        ::WorkItems::Widgets::Milestone,
-        ::WorkItems::Widgets::Notes
-      )
-    end
-  end
-
   describe '.default_by_type' do
     let(:default_issue_type) { described_class.find_by(namespace_id: nil, base_type: :issue) }
 
     subject { described_class.default_by_type(:issue) }
 
     it 'returns default work item type by base type without calling importer' do
-      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types)
+      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types).and_call_original
+      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_widgets)
       expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions)
 
       expect(subject).to eq(default_issue_type)
@@ -94,7 +93,8 @@
       end
 
       it 'creates types and restrictions and returns default work item type by base type' do
-        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types)
+        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types).and_call_original
+        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_widgets)
         expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions)
 
         expect(subject).to eq(default_issue_type)
@@ -128,19 +128,18 @@
   end
 
   describe '#supports_assignee?' do
-    subject(:supports_assignee) { build(:work_item_type, :task).supports_assignee? }
+    let_it_be_with_reload(:work_item_type) { create(:work_item_type) }
+    let_it_be_with_reload(:widget_definition) do
+      create(:widget_definition, work_item_type: work_item_type, widget_type: :assignees)
+    end
 
-    context 'when the assignees widget is supported' do
-      before do
-        stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Assignees] })
-      end
+    subject(:supports_assignee) { work_item_type.supports_assignee? }
 
-      it { is_expected.to be_truthy }
-    end
+    it { is_expected.to be_truthy }
 
     context 'when the assignees widget is not supported' do
       before do
-        stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [] })
+        widget_definition.update!(disabled: true)
       end
 
       it { is_expected.to be_falsey }
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..08f8f4d9663bd6828d2b651b4cc7dd9ff02b64e9
--- /dev/null
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
+  let(:all_widget_classes) do
+    list = [
+      ::WorkItems::Widgets::Description,
+      ::WorkItems::Widgets::Hierarchy,
+      ::WorkItems::Widgets::Labels,
+      ::WorkItems::Widgets::Assignees,
+      ::WorkItems::Widgets::StartAndDueDate,
+      ::WorkItems::Widgets::Milestone,
+      ::WorkItems::Widgets::Notes
+    ]
+
+    if Gitlab.ee?
+      list += [
+        ::WorkItems::Widgets::Iteration,
+        ::WorkItems::Widgets::Weight,
+        ::WorkItems::Widgets::Status,
+        ::WorkItems::Widgets::HealthStatus,
+        ::WorkItems::Widgets::Progress,
+        ::WorkItems::Widgets::RequirementLegacy,
+        ::WorkItems::Widgets::TestReports
+      ]
+    end
+
+    list
+  end
+
+  describe 'associations' do
+    it { is_expected.to belong_to(:namespace) }
+    it { is_expected.to belong_to(:work_item_type) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:name) }
+    it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id, :work_item_type_id]) }
+    it { is_expected.to validate_length_of(:name).is_at_most(255) }
+  end
+
+  context 'with some widgets disabled' do
+    before do
+      described_class.global.where(widget_type: :notes).update_all(disabled: true)
+    end
+
+    describe '.available_widgets' do
+      subject { described_class.available_widgets }
+
+      it 'returns all global widgets excluding the disabled ones' do
+        # WorkItems::Widgets::Notes is excluded from widget class because:
+        # * although widget_definition below is enabled and uses notes widget, it's namespaced (has namespace != nil)
+        # * available_widgets takes into account only global definitions (which have namespace=nil)
+        namespace = create(:namespace)
+        create(:widget_definition, namespace: namespace, widget_type: :notes)
+
+        is_expected.to match_array(all_widget_classes - [::WorkItems::Widgets::Notes])
+      end
+
+      it 'returns all global widgets if there is at least one global widget definition which is enabled' do
+        create(:widget_definition, namespace: nil, widget_type: :notes)
+
+        is_expected.to match_array(all_widget_classes)
+      end
+    end
+
+    describe '.widget_classes' do
+      subject { described_class.widget_classes }
+
+      it 'returns all widget classes no matter if disabled or not' do
+        is_expected.to match_array(all_widget_classes)
+      end
+    end
+  end
+
+  describe '#widget_class' do
+    it 'returns widget class based on widget_type' do
+      expect(build(:widget_definition, widget_type: :description).widget_class).to eq(::WorkItems::Widgets::Description)
+    end
+
+    it 'returns nil if there is no class for the widget_type' do
+      described_class.first.update_column(:widget_type, -1)
+
+      expect(described_class.first.widget_class).to be_nil
+    end
+
+    it 'returns nil if there is no class for the widget_type' do
+      expect(build(:widget_definition, widget_type: nil).widget_class).to be_nil
+    end
+  end
+end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 0040d9dff7e502aad05e866b11bb8333ae90478c..17558787966077b2c6e1ca1d38093fab21a4d99e 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -425,19 +425,15 @@ def permissions(user, issue)
     context 'when accounting for notes widget' do
       let(:policy) { described_class.new(reporter, note) }
 
-      before do
-        widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
-        widgets_per_type[:task] = [::WorkItems::Widgets::Description]
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
-      end
-
-      context 'and notes widget is disabled for task' do
-        let(:task) { create(:work_item, :task, project: project) }
+      context 'and notes widget is disabled for issue' do
+        before do
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
+        end
 
         it 'does not allow accessing notes' do
           # if notes widget is disabled not even maintainer can access notes
-          expect(permissions(maintainer, task)).to be_disallowed(:create_note, :read_note, :mark_note_as_internal, :read_internal_note)
-          expect(permissions(admin, task)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
+          expect(permissions(maintainer, issue)).to be_disallowed(:create_note, :read_note, :mark_note_as_internal, :read_internal_note)
+          expect(permissions(admin, issue)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
         end
       end
 
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index f4abe3a223c1c415a18743dc0c2626f95596feb2..b2191e6925df20a54fb15b501193d63022441853 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -260,9 +260,7 @@
             let(:policy) { described_class.new(developer, note) }
 
             before do
-              widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
-              widgets_per_type[:task] = [::WorkItems::Widgets::Description]
-              stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
+              WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
             end
 
             context 'when noteable is task' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 38016375b8f7c1f7aad7f3d5e1f3c785c2815534..c5126dbd1c2066d7287bdf1967fe2f8f9c7f5863 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -42,8 +42,7 @@
 
     context 'with work item without notes widget' do
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       context 'when fetching discussions' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 00e259097465a0697bcece2c93b775f356daf046..a6253ba424b168b1b9653674826672465e2b07e2 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -122,8 +122,8 @@ def mutation_response
           let(:variables_extra) { {} }
 
           before do
-            stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-            stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+            WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes)
+              .update!(disabled: true)
           end
 
           it_behaves_like 'a Note mutation that does not create a Note'
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index eb45e2aa033b42c3d9dafac0c87f0cf5bf694298..f40518a574b5ba0b6cb4c2e4b0d7b0f35e17dcbe 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -57,8 +57,7 @@ def mutation_response
 
     context 'without notes widget' do
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'does not update the Note' do
diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
index dff8a87314b84dbb04110295d32b80f4d2f4d574..7918bc860fe640bcf41e33ff7f5bb9642fcde393 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -50,8 +50,7 @@ def mutation_response
 
       context 'without notes widget' do
         before do
-          stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-          stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
         end
 
         it 'does not update the Note' do
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 271c2b917ad8d6f737ced35de05f9fcb6e88a231..ddd294e8f829d7eab03388f155fb13970ad46900 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -252,7 +252,8 @@
         let(:input) { { 'descriptionWidget' => { 'description' => "Updating labels.\n/labels ~\"#{label1.name}\"" } } }
 
         before do
-          stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:task).widget_definitions
+            .find_by_widget_type(:labels).update!(disabled: true)
         end
 
         it 'ignores the quick action' do
@@ -370,7 +371,8 @@
           let(:input) { { 'descriptionWidget' => { 'description' => "Updating due date.\n/due today" } } }
 
           before do
-            stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+            WorkItems::Type.default_by_type(:task).widget_definitions
+              .find_by_widget_type(:start_and_due_date).update!(disabled: true)
           end
 
           it 'ignores the quick action' do
@@ -736,7 +738,8 @@
         end
 
         before do
-          stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:task).widget_definitions
+            .find_by_widget_type(:assignees).update!(disabled: true)
         end
 
         it 'ignores the quick action' do
diff --git a/spec/requests/api/graphql/notes/note_spec.rb b/spec/requests/api/graphql/notes/note_spec.rb
index 180e54290f861d220cd1bc242c6d10edf1e87a6f..daceaec0b94cd6002e622d980284043df4a52486 100644
--- a/spec/requests/api/graphql/notes/note_spec.rb
+++ b/spec/requests/api/graphql/notes/note_spec.rb
@@ -66,7 +66,8 @@
 
     context 'and notes widget is not available' do
       before do
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions
+          .find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'returns nil' do
diff --git a/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
index 9b11406ae00ea531cefea6e295a8303d8b02dc0f..1199aeb4c39b201af2b18667d892341c9be589c3 100644
--- a/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
+++ b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
@@ -44,7 +44,8 @@
 
     context 'and notes widget is not available' do
       before do
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions
+          .find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'returns nil' do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index c2d9db1e6fbba1e15a09c74acf7aba6d2e12c65c..c0276e02eb78798e6657086ec64e4be4364ede4a 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -210,8 +210,7 @@
         let(:request_path) { "/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes" }
 
         before do
-          stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-          stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
         end
 
         it 'does not fetch notes' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index ecdd8d031c92550beee8ad7f1ca5f07f38e124a5..2252bb15d51df9059c3376a96ce3eb403fa95ea9 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -22,8 +22,7 @@
       let_it_be(:issuable) { create(:work_item, :issue, project: project) }
 
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it "returns no notes" do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 049c90f20b071e3ecc545d439df0f124782a3198..7c8b600032d4f9afd0080fbdd57cd3523e97a66f 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -188,7 +188,7 @@
           {
             title: 'Awesome work_item',
             description: 'please fix',
-            work_item_type: create(:work_item_type, :task)
+            work_item_type: WorkItems::Type.default_by_type(:task)
           }
         end
 
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 588fe466a42ae82a2f772a6065aaeb63b7cd1d38..b9a99eff41359c15319352e57fa5c9c0c0332123 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -12,7 +12,7 @@ def delete_from_all_tables!(except: [])
   end
 
   def deletion_except_tables
-    %w[work_item_types work_item_hierarchy_restrictions]
+    %w[work_item_types work_item_hierarchy_restrictions work_item_widget_definitions]
   end
 
   def setup_database_cleaner
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
index b10110375849551ad91612ebd4e988219529f7ba..1703d400aeaaeeff3ca1e9c37d20af3044ff622b 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -15,6 +15,22 @@
     expect(WorkItems::Type.all).to all(be_valid)
   end
 
+  it 'creates all default widget definitions' do
+    WorkItems::WidgetDefinition.delete_all
+    widget_mapping = ::Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE
+
+    expect { subject }.to change { WorkItems::WidgetDefinition.count }.from(0).to(widget_mapping.values.flatten.count)
+
+    created_widgets = WorkItems::WidgetDefinition.global.map do |widget|
+      { name: widget.work_item_type.name, type: widget.widget_type }
+    end
+    expected_widgets = widget_mapping.flat_map do |type_sym, widget_types|
+      widget_types.map { |type| { name: ::WorkItems::Type::TYPE_NAMES[type_sym], type: type.to_s } }
+    end
+
+    expect(created_widgets).to match_array(expected_widgets)
+  end
+
   it 'upserts base work item types if they already exist' do
     first_type = WorkItems::Type.first
     original_name = first_type.name
@@ -29,8 +45,34 @@
     )
   end
 
-  it 'executes a single INSERT query' do
-    expect { subject }.to make_queries_matching(/INSERT/, 1)
+  it 'upserts default widget definitions if they already exist and type changes' do
+    widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+    widget.update!(widget_type: :weight)
+
+    expect do
+      subject
+      widget.reload
+    end.to not_change(WorkItems::WidgetDefinition, :count).and(
+      change { widget.widget_type }.from('weight').to('labels')
+    )
+  end
+
+  it 'does not change default widget definitions if they already exist with changed disabled status' do
+    widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+    widget.update!(disabled: true)
+
+    expect do
+      subject
+      widget.reload
+    end.to not_change(WorkItems::WidgetDefinition, :count).and(
+      not_change { widget.disabled }
+    )
+  end
+
+  it 'executes single INSERT query per types and widget definitions' do
+    expect { subject }.to make_queries_matching(/INSERT/, 2)
   end
 
   context 'when some base types exist' do
@@ -39,10 +81,22 @@
     end
 
     it 'inserts all types and does nothing if some already existed' do
-      expect { subject }.to make_queries_matching(/INSERT/, 1).and(
+      expect { subject }.to make_queries_matching(/INSERT/, 2).and(
         change { WorkItems::Type.count }.by(1)
       )
       expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count)
     end
   end
+
+  context 'when some widget definitions exist' do
+    before do
+      WorkItems::WidgetDefinition.limit(1).delete_all
+    end
+
+    it 'inserts all widget definitions and does nothing if some already existed' do
+      expect { subject }.to make_queries_matching(/INSERT/, 2).and(
+        change { WorkItems::WidgetDefinition.count }.by(1)
+      )
+    end
+  end
 end