diff --git a/group.install b/group.install
index 49d1dd4..b772ced 100644
--- a/group.install
+++ b/group.install
@@ -424,3 +424,17 @@ function group_update_8014() {
     $group_type->save(TRUE);
   }
 }
+
+/**
+ * Install 'group_config' entity type.
+ */
+function group_update_8015() {
+  $container = \Drupal::getContainer();
+
+  /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager */
+  $entity_manager = $container->get('entity_type.manager');
+  $storage_handler = $entity_manager->getHandler('group_config', 'storage');
+  $entity_type = $entity_manager->getDefinition('group_config');
+  $new_storage = $entity_manager->createHandlerInstance($storage_handler, $entity_type);
+  $new_storage->onEntityTypeCreate($entity_type);
+}
diff --git a/group.links.action.yml b/group.links.action.yml
index 04f0ca8..c414074 100644
--- a/group.links.action.yml
+++ b/group.links.action.yml
@@ -36,3 +36,9 @@ group_content.create_page:
   title: 'Create new entity in group'
   appears_on:
     - 'entity.group_content.collection'
+
+group_config.add_page:
+  route_name: 'entity.group_config.add_page'
+  title: 'Relate existing config entity to group'
+  appears_on:
+    - 'entity.group_config.collection'
diff --git a/group.links.task.yml b/group.links.task.yml
index 7867843..c8250fd 100644
--- a/group.links.task.yml
+++ b/group.links.task.yml
@@ -35,6 +35,12 @@ group.content:
   route_name: 'entity.group_content.collection'
   weight: 15
 
+group.config:
+  title: 'Related config entities'
+  base_route: 'entity.group.canonical'
+  route_name: 'entity.group_config.collection'
+  weight: 15
+
 group_type.edit_form:
   title: 'Edit'
   base_route: 'entity.group_type.edit_form'
@@ -71,6 +77,12 @@ group_type.content_plugins:
   route_name: 'entity.group_type.content_plugins'
   weight: 25
 
+group_type.config_plugins:
+  title: 'Config'
+  base_route: 'entity.group_type.edit_form'
+  route_name: 'entity.group_type.config_plugins'
+  weight: 25
+
 group_content_type.edit_form:
   title: 'Configure'
   base_route: 'entity.group_content_type.edit_form'
@@ -109,3 +121,20 @@ group_content.delete:
   base_route: 'entity.group_content.canonical'
   route_name: 'entity.group_content.delete_form'
   weight: 10
+
+group_config.view:
+  title: 'View'
+  base_route: 'entity.group_config.canonical'
+  route_name: 'entity.group_config.canonical'
+
+group_config.edit:
+  title: 'Edit'
+  base_route: 'entity.group_config.canonical'
+  route_name: 'entity.group_config.edit_form'
+  weight: 5
+
+group_config.delete:
+  title: 'Remove'
+  base_route: 'entity.group_config.canonical'
+  route_name: 'entity.group_config.delete_form'
+  weight: 10
diff --git a/group.module b/group.module
index 046b178..281ac1a 100644
--- a/group.module
+++ b/group.module
@@ -15,6 +15,7 @@ use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\group\Entity\GroupContent;
+use Drupal\group\Entity\GroupConfig;
 use Drupal\user\RoleInterface;
 
 /**
@@ -23,7 +24,10 @@ use Drupal\user\RoleInterface;
 function group_help($route_name, RouteMatchInterface $route_match) {
   switch ($route_name) {
     case 'entity.group_type.content_plugins':
-      return '<p>' . t('Entities that can be added to this group type.') . '</p>';
+      return '<p>' . t('Content Entities that can be added to this group type.') . '</p>';
+
+    case 'entity.group_type.config_plugins':
+      return '<p>' . t('Config Entities that can be added to this group type.') . '</p>';
   }
 }
 
@@ -48,6 +52,9 @@ function group_theme() {
     'group_content' => [
       'render element' => 'elements',
     ],
+    'group_config' => [
+      'render element' => 'elements',
+    ],
   ];
 }
 
@@ -90,9 +97,28 @@ function group_theme_suggestions_group_content(array $variables) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function group_theme_suggestions_group_config(array $variables) {
+  $suggestions = [];
+
+  /** @var \Drupal\group\Entity\GroupConfigInterface $group_config */
+  $group_config = $variables['elements']['#group_config'];
+  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
+
+  $suggestions[] = 'group_config__' . $sanitized_view_mode;
+  $suggestions[] = 'group_config__' . $group_config->bundle();
+  $suggestions[] = 'group_config__' . $group_config->bundle() . '__' . $sanitized_view_mode;
+  $suggestions[] = 'group_config__' . $group_config->id();
+  $suggestions[] = 'group_config__' . $group_config->id() . '__' . $sanitized_view_mode;
+
+  return $suggestions;
+}
+
+/**
  * Prepares variables for the group template.
  *
- * Default template: group.html.twig
+ * Default template: group.html.twig.
  *
  * @param array $variables
  *   - elements: An array of elements to display in view mode.
@@ -126,7 +152,7 @@ function template_preprocess_group(&$variables) {
 /**
  * Prepares variables for the group content template.
  *
- * Default template: group-content.html.twig
+ * Default template: group-content.html.twig.
  *
  * @param array $variables
  *   - elements: An array of elements to display in view mode.
@@ -158,10 +184,45 @@ function template_preprocess_group_content(&$variables) {
 }
 
 /**
+ * Prepares variables for the group content template.
+ *
+ * Default template: group-content.html.twig.
+ *
+ * @param array $variables
+ *   - elements: An array of elements to display in view mode.
+ *   - group_config: The group content object.
+ *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
+ */
+function template_preprocess_group_config(&$variables) {
+  /** @var \Drupal\group\Entity\GroupContentInterface $group_config */
+  $group_config = $variables['elements']['#group_config'];
+
+  $variables['group_config'] = $group_config;
+  $variables['view_mode'] = $variables['elements']['#view_mode'];
+  $variables['label'] = $group_config->label();
+  $variables['url'] = $group_config->toUrl('canonical', ['language' => $group_config->language()]);
+
+  // See if we are rendering the group at its canonical route.
+  $route_match = \Drupal::routeMatch();
+  if ($route_match->getRouteName() == 'entity.group_config.canonical') {
+    $page_group_config = $route_match->getParameter('group_config');
+  }
+  $is_page = (!empty($page_group_config) ? $page_group_config->id() == $group_config->id() : FALSE);
+  $variables['page'] = $variables['view_mode'] == 'full' && $is_page;
+
+  // Helpful $content variable for templates.
+  $variables += ['content' => []];
+  foreach (Element::children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+}
+
+/**
  * Implements hook_rebuild().
  */
 function group_rebuild() {
   _group_content_enabler_manager()->installEnforced();
+  _group_config_enabler_manager()->installEnforced();
   _group_role_synchronizer()->createGroupRoles();
 }
 
@@ -170,6 +231,7 @@ function group_rebuild() {
  */
 function group_modules_installed($modules) {
   _group_content_enabler_manager()->installEnforced();
+  _group_config_enabler_manager()->installEnforced();
   _group_role_synchronizer()->createGroupRoles();
 }
 
@@ -201,6 +263,12 @@ function group_entity_delete(EntityInterface $entity) {
         $group_content->delete();
       }
     }
+    if ($group_configs = GroupConfig::loadByEntity($entity)) {
+      /** @var \Drupal\group\Entity\GroupConfig $group_config */
+      foreach ($group_configs as $group_config) {
+        $group_config->delete();
+      }
+    }
   }
 }
 
@@ -510,7 +578,6 @@ function group_content_entity_submit($form, FormStateInterface $form_state) {
  */
 
 // Nothing to see here yet, our own access hook implementations will go here.
-
 /**
  * @} End of "defgroup group_access".
  */
@@ -528,6 +595,18 @@ function _group_content_enabler_manager() {
 }
 
 /**
+ * Gets the group config enabler plugin manager.
+ *
+ * @return \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+ *   The group config enabler plugin manager.
+ *
+ * @internal Try to properly inject the service when possible.
+ */
+function _group_config_enabler_manager() {
+  return \Drupal::service('plugin.manager.group_config_enabler');
+}
+
+/**
  * Gets the group role synchronizer service.
  *
  * @return \Drupal\group\GroupRoleSynchronizerInterface
diff --git a/group.routing.yml b/group.routing.yml
index b851a2d..f344d59 100644
--- a/group.routing.yml
+++ b/group.routing.yml
@@ -52,6 +52,14 @@ entity.group_type.content_plugins:
   requirements:
     _permission: 'administer group'
 
+entity.group_type.config_plugins:
+  path: '/admin/group/types/manage/{group_type}/config'
+  defaults:
+    _controller: '\Drupal\group\Entity\Controller\GroupTypeController::config_entity'
+    _title: 'Configure available content'
+  requirements:
+    _permission: 'administer group'
+
 # Group role entity routes.
 # Common entity routes are generated by \Drupal\group\Entity\Routing\GroupRoleRouteProvider.
 entity.group_role.permissions_form:
@@ -95,3 +103,28 @@ entity.group_content_type.delete_form:
     _title: 'Uninstall content plugin'
   requirements:
     _permission: 'administer group'
+
+# Group config type entity routes.
+entity.group_config_type.add_form:
+  path: '/admin/group/config/install/{group_type}/{plugin_id}'
+  defaults:
+    _entity_form: 'group_config_type.add'
+    _title: 'Install config plugin'
+  requirements:
+    _permission: 'administer group'
+
+entity.group_config_type.edit_form:
+  path: '/admin/group/config/manage/{group_config_type}'
+  defaults:
+    _entity_form: 'group_config_type.edit'
+    _title: 'Configure config plugin'
+  requirements:
+    _permission: 'administer group'
+
+entity.group_config_type.delete_form:
+  path: '/admin/group/config/manage/{group_config_type}/uninstall'
+  defaults:
+    _entity_form: 'group_config_type.delete'
+    _title: 'Uninstall config plugin'
+  requirements:
+    _permission: 'administer group'
diff --git a/group.services.yml b/group.services.yml
index 95b34e7..acfddb3 100644
--- a/group.services.yml
+++ b/group.services.yml
@@ -81,7 +81,7 @@ services:
     arguments: ['@entity_type.manager', '@current_user']
   group.permissions:
     class: 'Drupal\group\Access\GroupPermissionHandler'
-    arguments: ['@module_handler', '@string_translation', '@controller_resolver', '@plugin.manager.group_content_enabler']
+    arguments: ['@module_handler', '@string_translation', '@controller_resolver', '@plugin.manager.group_content_enabler', '@plugin.manager.group_config_enabler']
   group.permissions_hash_generator:
     class: 'Drupal\group\Access\GroupPermissionsHashGenerator'
     arguments: ['@private_key', '@cache.default', '@cache.static', '@entity_type.manager']
@@ -106,3 +106,46 @@ services:
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']
     tags:
       - { name: plugin_manager_cache_clear }
+
+  access_check.group.installed_config:
+    class: 'Drupal\group\Access\GroupInstalledConfigAccessCheck'
+    tags:
+      - { name: 'access_check', applies_to: '_group_installed_config' }
+  access_check.group.owns_config:
+    class: 'Drupal\group\Access\GroupOwnsConfigAccessCheck'
+    tags:
+      - { name: 'access_check', applies_to: '_group_owns_config' }
+  access_check.group_config.create:
+    class: 'Drupal\group\Access\GroupConfigCreateAccessCheck'
+    arguments: ['@entity.manager']
+    tags:
+      - { name: 'access_check', applies_to: '_group_config_create_access' }
+  access_check.group_config.create_any:
+    class: 'Drupal\group\Access\GroupConfigCreateAnyAccessCheck'
+    arguments: ['@entity.manager']
+    tags:
+      - { name: 'access_check', applies_to: '_group_config_create_any_access' }
+  access_check.group_config.create_entity:
+    class: 'Drupal\group\Access\GroupConfigCreateEntityAccessCheck'
+    tags:
+      - { name: 'access_check', applies_to: '_group_config_create_entity_access' }
+  access_check.group_config.create_any_entity:
+    class: 'Drupal\group\Access\GroupConfigCreateAnyEntityAccessCheck'
+    tags:
+      - { name: 'access_check', applies_to: '_group_config_create_any_entity_access' }
+  group.uninstall_validator.group_config:
+    class: 'Drupal\group\UninstallValidator\GroupConfigUninstallValidator'
+    tags:
+      - { name: 'module_install.uninstall_validator' }
+    arguments: ['@string_translation', '@entity.query', '@plugin.manager.group_config_enabler']
+    lazy: 'true'
+
+  group_config_type.breadcrumb:
+    class: 'Drupal\group\Breadcrumb\GroupConfigTypeBreadcrumbBuilder'
+    tags:
+      - { name: 'breadcrumb_builder', priority: 100 }
+  plugin.manager.group_config_enabler:
+    class: 'Drupal\group\Plugin\GroupConfigEnablerManager'
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']
+    tags:
+      - { name: plugin_manager_cache_clear }
diff --git a/modules/gnode/src/Plugin/GroupConfigEnabler/GroupNodeType.php b/modules/gnode/src/Plugin/GroupConfigEnabler/GroupNodeType.php
new file mode 100644
index 0000000..c57a60d
--- /dev/null
+++ b/modules/gnode/src/Plugin/GroupConfigEnabler/GroupNodeType.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\gnode\Plugin\GroupConfigEnabler;
+
+use Drupal\group\Plugin\GroupConfigEnablerBase;
+
+/**
+ * Provides a content enabler for nodes.
+ *
+ * @GroupConfigEnabler(
+ *   id = "group_node_type",
+ *   label = @Translation("Group content type"),
+ *   description = @Translation("Adds content type to groups both publicly and privately."),
+ *   entity_type_id = "node_type",
+ * )
+ */
+class GroupNodeType extends GroupConfigEnablerBase {
+}
diff --git a/src/Access/GroupConfigCreateAccessCheck.php b/src/Access/GroupConfigCreateAccessCheck.php
new file mode 100644
index 0000000..64fa203
--- /dev/null
+++ b/src/Access/GroupConfigCreateAccessCheck.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\group\Entity\GroupInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access for group config creation.
+ */
+class GroupConfigCreateAccessCheck implements AccessInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a EntityCreateAccessCheck object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Checks access for group config creation routes.
+   *
+   * All routes using this access check should have a group and plugin_id
+   * parameter and have the _group_config_create_access requirement set to
+   * either 'TRUE' or 'FALSE'.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group in which the config should be created.
+   * @param string $plugin_id
+   *   The group config enabler ID to use for the group config entity.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, GroupInterface $group, $plugin_id) {
+    $needs_access = $route->getRequirement('_group_config_create_access') === 'TRUE';
+
+    // We can only get the group config type ID if the plugin is installed.
+    if (!$group->getGroupType()->hasConfigPlugin($plugin_id)) {
+      return AccessResult::neutral();
+    }
+
+    // Determine whether the user can create group config using the plugin.
+    $group_config_type_id = $group->getGroupType()->getConfigPlugin($plugin_id)->getConfigTypeConfigId();
+    $access_control_handler = $this->entityTypeManager->getAccessControlHandler('group_config');
+    $access = $access_control_handler->createAccess($group_config_type_id, $account, ['group' => $group]);
+
+    // Only allow access if the user can create group config using the
+    // provided plugin or if he doesn't need access to do so.
+    return AccessResult::allowedIf($access xor !$needs_access);
+  }
+
+}
diff --git a/src/Access/GroupConfigCreateAnyAccessCheck.php b/src/Access/GroupConfigCreateAnyAccessCheck.php
new file mode 100644
index 0000000..1a0af00
--- /dev/null
+++ b/src/Access/GroupConfigCreateAnyAccessCheck.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\group\Entity\GroupInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access for group config creation.
+ */
+class GroupConfigCreateAnyAccessCheck implements AccessInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a EntityCreateAccessCheck object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Checks access for group config creation routes.
+   *
+   * All routes using this access check should have a group parameter and have
+   * the _group_config_create_any_access requirement set to 'TRUE' or 'FALSE'.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group in which the config should be created.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, GroupInterface $group) {
+    $needs_access = $route->getRequirement('_group_config_create_any_access') === 'TRUE';
+
+    // Get the group config access control handler.
+    $access_control_handler = $this->entityTypeManager->getAccessControlHandler('group_config');
+
+    // Retrieve all of the group config type IDs for the group.
+    $storage = $this->entityTypeManager->getStorage('group_config_type');
+    $entity_query = $storage->getQuery();
+    $entity_query->condition('group_type', $group->bundle());
+    $group_config_type_ids = $entity_query->execute();
+
+    // Find out which ones the user has access to create.
+    foreach ($group_config_type_ids as $group_config_type_id) {
+      if ($access_control_handler->createAccess($group_config_type_id, $account, ['group' => $group])) {
+        // Allow access if the route flag was set to 'TRUE'.
+        return AccessResult::allowedIf($needs_access);
+      }
+    }
+
+    // If we got this far, it means the user could not create any config in the
+    // group. So only allow access if the route flag was set to 'FALSE'.
+    return AccessResult::allowedIf(!$needs_access);
+  }
+
+}
diff --git a/src/Access/GroupConfigCreateAnyEntityAccessCheck.php b/src/Access/GroupConfigCreateAnyEntityAccessCheck.php
new file mode 100644
index 0000000..7d38e6f
--- /dev/null
+++ b/src/Access/GroupConfigCreateAnyEntityAccessCheck.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\group\Entity\GroupInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access for group config target entity creation.
+ */
+class GroupConfigCreateAnyEntityAccessCheck implements AccessInterface {
+
+  /**
+   * Checks access for group config target entity creation routes.
+   *
+   * All routes using this access check should have a group parameter and have
+   * the _group_config_create_any_entity_access requirement set to 'TRUE' or
+   * 'FALSE'.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group in which the config should be created.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, GroupInterface $group) {
+    $needs_access = $route->getRequirement('_group_config_create_any_entity_access') === 'TRUE';
+
+    // Retrieve all of the group config plugins for the group.
+    $plugins = $group->getGroupType()->getInstalledConfigPlugins();
+
+    // Find out which ones allow the user to create a target entity.
+    foreach ($plugins as $plugin) {
+      /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+      if ($plugin->createEntityAccess($group, $account)->isAllowed()) {
+        // Allow access if the route flag was set to 'TRUE'.
+        return AccessResult::allowedIf($needs_access);
+      }
+    }
+
+    // If we got this far, it means the user could not create any config in the
+    // group. So only allow access if the route flag was set to 'FALSE'.
+    return AccessResult::allowedIf(!$needs_access);
+  }
+
+}
diff --git a/src/Access/GroupConfigCreateEntityAccessCheck.php b/src/Access/GroupConfigCreateEntityAccessCheck.php
new file mode 100644
index 0000000..d06f3f4
--- /dev/null
+++ b/src/Access/GroupConfigCreateEntityAccessCheck.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\group\Entity\GroupInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access for group config target entity creation.
+ */
+class GroupConfigCreateEntityAccessCheck implements AccessInterface {
+
+  /**
+   * Checks access for group config target entity creation routes.
+   *
+   * All routes using this access check should have a group and plugin_id
+   * parameter and have the _group_config_create_entity_access requirement set
+   * to either 'TRUE' or 'FALSE'.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group in which the config should be created.
+   * @param string $plugin_id
+   *   The group config enabler ID to use for the group config entity.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, GroupInterface $group, $plugin_id) {
+    $needs_access = $route->getRequirement('_group_config_create_entity_access') === 'TRUE';
+
+    // We can only get the group config type ID if the plugin is installed.
+    if (!$group->getGroupType()->hasConfigPlugin($plugin_id)) {
+      return AccessResult::neutral();
+    }
+
+    // Determine whether the user can create entities of the provided type.
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+    $access = $plugin->createEntityAccess($group, $account)->isAllowed();
+
+    // Only allow access if the user can create group config target entities
+    // using the provided plugin or if he doesn't need access to do so.
+    return AccessResult::allowedIf($access xor !$needs_access);
+  }
+
+}
diff --git a/src/Access/GroupInstalledConfigAccessCheck.php b/src/Access/GroupInstalledConfigAccessCheck.php
new file mode 100644
index 0000000..8a14765
--- /dev/null
+++ b/src/Access/GroupInstalledConfigAccessCheck.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\group\Entity\GroupInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access to routes based on whether a config plugin is installed.
+ */
+class GroupInstalledConfigAccessCheck implements AccessInterface {
+
+  /**
+   * Checks access.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The parametrized route.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account to check access for.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
+    $access_string = $route->getRequirement('_group_installed_config');
+
+    // Don't interfere if no plugin ID was specified.
+    if ($access_string === NULL) {
+      return AccessResult::neutral();
+    }
+
+    // Don't interfere if no group was specified.
+    $parameters = $route_match->getParameters();
+    if (!$parameters->has('group')) {
+      return AccessResult::neutral();
+    }
+
+    // Don't interfere if the group isn't a real group.
+    $group = $parameters->get('group');
+    if (!$group instanceof GroupInterface) {
+      return AccessResult::neutral();
+    }
+
+    // We default to not granting access.
+    $access = FALSE;
+
+    // Allow to conjunct the plugin IDs with OR ('+') or AND (',').
+    $plugin_ids = explode(',', $access_string);
+    if (count($plugin_ids) > 1) {
+      $access = TRUE;
+
+      foreach ($plugin_ids as $plugin_id) {
+        if (!$group->getGroupType()->hasConfigPlugin($plugin_id)) {
+          $access = FALSE;
+          break;
+        }
+      }
+    }
+    else {
+      $plugin_ids = explode('+', $access_string);
+      foreach ($plugin_ids as $plugin_id) {
+        if ($group->getGroupType()->hasConfigPlugin($plugin_id)) {
+          $access = TRUE;
+          break;
+        }
+      }
+    }
+
+    return AccessResult::allowedIf($access);
+  }
+
+}
diff --git a/src/Access/GroupOwnsConfigAccessCheck.php b/src/Access/GroupOwnsConfigAccessCheck.php
new file mode 100644
index 0000000..39ff13a
--- /dev/null
+++ b/src/Access/GroupOwnsConfigAccessCheck.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\group\Access;
+
+use Drupal\group\Entity\GroupInterface;
+use Drupal\group\Entity\GroupConfigInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access to routes based on whether a piece of group config belongs
+ * to the group that was also specified in the route.
+ */
+class GroupOwnsConfigAccessCheck implements AccessInterface {
+
+  /**
+   * Checks access.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The parametrized route.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account to check access for.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
+    $must_own_config = $route->getRequirement('_group_owns_config') === 'TRUE';
+
+    // Don't interfere if no group or group config was specified.
+    $parameters = $route_match->getParameters();
+    if (!$parameters->has('group') || !$parameters->has('group_config')) {
+      return AccessResult::neutral();
+    }
+
+    // Don't interfere if the group isn't a real group.
+    $group = $parameters->get('group');
+    if (!$group instanceof GroupInterface) {
+      return AccessResult::neutral();
+    }
+
+    // Don't interfere if the group config isn't a real group config entity.
+    $group_config = $parameters->get('group_config');
+    if (!$group_config instanceof GroupConfigInterface) {
+      return AccessResult::neutral();
+    }
+
+    // If we have a group and group config, see if the owner matches.
+    $group_owns_config = $group_config->getGroup()->id() == $group->id();
+
+    // Only allow access if the group config is owned by the group and
+    // _group_owns_config is set to TRUE or the other way around.
+    return AccessResult::allowedIf($group_owns_config xor !$must_own_config);
+  }
+
+}
diff --git a/src/Access/GroupPermissionHandler.php b/src/Access/GroupPermissionHandler.php
index 2726c7d..b4f779f 100644
--- a/src/Access/GroupPermissionHandler.php
+++ b/src/Access/GroupPermissionHandler.php
@@ -148,6 +148,15 @@ class GroupPermissionHandler implements GroupPermissionHandlerInterface {
       }
     }
 
+    // Add the plugin defined permissions to the whole.
+    foreach ($group_type->getInstalledConfigPlugins() as $plugin) {
+      /** @var \Drupal\group\Plugin\GroupContentEnablerInterface $plugin */
+      foreach ($plugin->getPermissions() as $permission_name => $permission) {
+        $permission += ['provider' => $plugin->getProvider()];
+        $all_permissions[$permission_name] = $this->completePermission($permission);
+      }
+    }
+
     return $this->sortPermissions($all_permissions);
   }
 
@@ -226,7 +235,7 @@ class GroupPermissionHandler implements GroupPermissionHandlerInterface {
           $permission = ['title' => $permission];
         }
 
-        // Set the provider if none was spec
+        // Set the provider if none was spec.
         $permissions[$permission_name] = $permission + ['provider' => $provider];
       }
 
diff --git a/src/Annotation/GroupConfigEnabler.php b/src/Annotation/GroupConfigEnabler.php
new file mode 100644
index 0000000..325d4db
--- /dev/null
+++ b/src/Annotation/GroupConfigEnabler.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\group\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a GroupConfigEnabler annotation object.
+ *
+ * Plugin Namespace: Plugin\GroupConfigEnabler.
+ *
+ * For a working example, see
+ * \Drupal\group\Plugin\GroupConfigEnabler\GroupMembership
+ *
+ * @see \Drupal\group\Plugin\GroupConfigEnablerInterface
+ * @see \Drupal\group\Plugin\GroupConfigEnablerManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class GroupConfigEnabler extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the GroupConfigEnabler plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * A short description of the GroupConfigEnabler plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description;
+
+  /**
+   * The ID of the entity type you want to enable as group config.
+   *
+   * @var string
+   */
+  public $entity_type_id;
+
+  /**
+   * (optional) The bundle of the entity type you want to enable as group config.
+   *
+   * Do not specify if your plugin manages all bundles.
+   *
+   * @var string|false
+   */
+  public $entity_bundle = FALSE;
+
+  /**
+   * (optional) Whether the plugin defines entity access.
+   *
+   * This controls whether you can create entities within the group (TRUE) or
+   * only add existing ones (FALSE). It also generates the necessary group
+   * permissions when enabled.
+   *
+   * Eventually, this will even generate entity access records for you, but that
+   * will only happen after the patch in https://www.drupal.org/node/777578 has
+   * been committed to Drupal core.
+   *
+   * @var bool
+   */
+  public $entity_access = FALSE;
+
+  /**
+   * (optional) The key to use in automatically generated paths.
+   *
+   * This is exposed through tokens so modules like Pathauto may use it. Only
+   * use this if your plugin has something meaningful to show on the actual
+   * group config entity; i.e.: the relationship. Otherwise leave blank so it
+   * defaults to 'config'.
+   *
+   * @var string
+   */
+  public $pretty_path_key = 'config';
+
+  /**
+   * (optional) The label for the entity reference field.
+   *
+   * @var string
+   */
+  public $reference_label;
+
+  /**
+   * (optional) The description for the entity reference field.
+   *
+   * @var string
+   */
+  public $reference_description;
+
+  /**
+   * (optional) Whether this plugin is always on.
+   *
+   * @var bool
+   */
+  public $enforced = FALSE;
+
+}
diff --git a/src/Breadcrumb/GroupConfigTypeBreadcrumbBuilder.php b/src/Breadcrumb/GroupConfigTypeBreadcrumbBuilder.php
new file mode 100644
index 0000000..806398b
--- /dev/null
+++ b/src/Breadcrumb/GroupConfigTypeBreadcrumbBuilder.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\group\Breadcrumb;
+
+use Drupal\group\Entity\GroupConfigTypeInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Provides a custom breadcrumb builder for group config type paths.
+ */
+class GroupConfigTypeBreadcrumbBuilder implements BreadcrumbBuilderInterface {
+  use StringTranslationTrait;
+
+  /**
+   * @inheritdoc
+   */
+  public function applies(RouteMatchInterface $route_match) {
+    // Only apply to paths containing a group config type.
+    if ($route_match->getParameter('group_config_type') instanceof GroupConfigTypeInterface) {
+      return TRUE;
+    }
+  }
+
+  /**
+   * @inheritdoc
+   */
+  public function build(RouteMatchInterface $route_match) {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $route_match->getParameter('group_config_type');
+    $group_type = $group_config_type->getGroupType();
+
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Administration'), 'system.admin'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Groups'), 'entity.group.collection'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Group types'), 'entity.group_type.collection'));
+    $breadcrumb->addLink(Link::createFromRoute($group_type->label(), 'entity.group_type.edit_form', ['group_type' => $group_type->id()]));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Config'), 'entity.group_type.config_plugins', ['group_type' => $group_type->id()]));
+
+    // Add a link to the Configure page for any non-default tab.
+    if ($route_match->getRouteName() != 'entity.group_config_type.edit_form') {
+      $breadcrumb->addLink(Link::createFromRoute($this->t('Configure'), 'entity.group_config_type.edit_form', ['group_config_type' => $group_config_type->id()]));
+    }
+
+    // Breadcrumb needs to have the group type and group config type as
+    // cacheable dependencies because any changes to them should be reflected.
+    $breadcrumb->addCacheableDependency($group_type);
+    $breadcrumb->addCacheableDependency($group_config_type);
+
+    // This breadcrumb builder is based on a route parameter, and hence it
+    // depends on the 'route' cache context.
+    $breadcrumb->addCacheContexts(['route']);
+
+    return $breadcrumb;
+  }
+
+}
diff --git a/src/Entity/Access/GroupConfigAccessControlHandler.php b/src/Entity/Access/GroupConfigAccessControlHandler.php
new file mode 100644
index 0000000..7d5268a
--- /dev/null
+++ b/src/Entity/Access/GroupConfigAccessControlHandler.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\group\Entity\Access;
+
+use Drupal\group\Entity\GroupConfigType;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Access controller for the Group entity.
+ *
+ * @see \Drupal\group\Entity\Group.
+ */
+class GroupConfigAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\group\Entity\GroupConfigInterface $entity */
+    return $entity->getConfigPlugin()->checkAccess($entity, $operation, $account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    $group_config_type = GroupConfigType::load($entity_bundle);
+    return $group_config_type->getConfigPlugin()->createAccess($context['group'], $account);
+  }
+
+}
diff --git a/src/Entity/Access/GroupConfigTypeAccessControlHandler.php b/src/Entity/Access/GroupConfigTypeAccessControlHandler.php
new file mode 100644
index 0000000..a41df5b
--- /dev/null
+++ b/src/Entity/Access/GroupConfigTypeAccessControlHandler.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\group\Entity\Access;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the group config type entity type.
+ *
+ * @see \Drupal\group\Entity\GroupConfigType
+ */
+class GroupConfigTypeAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $entity */
+    if ($operation == 'delete') {
+      return parent::checkAccess($entity, $operation, $account)->addCacheableDependency($entity);
+    }
+    return parent::checkAccess($entity, $operation, $account);
+  }
+
+}
diff --git a/src/Entity/Controller/GroupConfigController.php b/src/Entity/Controller/GroupConfigController.php
new file mode 100644
index 0000000..d621e11
--- /dev/null
+++ b/src/Entity/Controller/GroupConfigController.php
@@ -0,0 +1,382 @@
+<?php
+
+namespace Drupal\group\Entity\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Entity\EntityFormBuilderInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+use Drupal\group\Entity\GroupConfigType;
+use Drupal\group\Entity\GroupInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Returns responses for GroupConfig routes.
+ */
+class GroupConfigController extends ControllerBase {
+
+  /**
+   * The private store factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $privateTempStoreFactory;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity form builder.
+   *
+   * @var \Drupal\Core\Entity\EntityFormBuilderInterface
+   */
+  protected $entityFormBuilder;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new GroupConfigController.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   *   The private store factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
+   *   The entity form builder.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, RendererInterface $renderer) {
+    $this->privateTempStoreFactory = $temp_store_factory;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityFormBuilder = $entity_form_builder;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('entity_type.manager'),
+      $container->get('entity.form_builder'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * Provides the group config creation overview page.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param bool $create_mode
+   *   (optional) Whether the target entity still needs to be created. Defaults
+   *   to FALSE, meaning the target entity is assumed to exist already.
+   *
+   * @return array|\Symfony\Component\HttpFoundation\RedirectResponse
+   *   The group config creation overview page or a redirect to the form for
+   *   adding group config if there is only one group config type.
+   */
+  public function addPage(GroupInterface $group, $create_mode = FALSE) {
+    $build = ['#theme' => 'entity_add_list', '#bundles' => []];
+    $form_route = $this->addPageFormRoute($group, $create_mode);
+    $bundle_names = $this->addPageBundles($group, $create_mode);
+
+    // Set the add bundle message if available.
+    $add_bundle_message = $this->addPageBundleMessage($group, $create_mode);
+    if ($add_bundle_message !== FALSE) {
+      $build['#add_bundle_message'] = $add_bundle_message;
+    }
+
+    // Filter out the bundles the user doesn't have access to.
+    $access_control_handler = $this->entityTypeManager->getAccessControlHandler('group_config');
+    foreach ($bundle_names as $plugin_id => $bundle_name) {
+      $access = $access_control_handler->createAccess($bundle_name, NULL, ['group' => $group], TRUE);
+      if (!$access->isAllowed()) {
+        unset($bundle_names[$plugin_id]);
+      }
+      $this->renderer->addCacheableDependency($build, $access);
+    }
+
+    // Redirect if there's only one bundle available.
+    if (count($bundle_names) == 1) {
+      reset($bundle_names);
+      $route_params = ['group' => $group->id(), 'plugin_id' => key($bundle_names)];
+      $url = Url::fromRoute($form_route, $route_params, ['absolute' => TRUE]);
+      return new RedirectResponse($url->toString());
+    }
+
+    // Set the info for all of the remaining bundles.
+    foreach ($bundle_names as $plugin_id => $bundle_name) {
+      $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+      $label = $plugin->getLabel();
+
+      $build['#bundles'][$bundle_name] = [
+        'label' => $label,
+        'description' => $plugin->getConfigTypeDescription(),
+        'add_link' => Link::createFromRoute($label, $form_route, ['group' => $group->id(), 'plugin_id' => $plugin_id]),
+      ];
+    }
+
+    // Add the list cache tags for the GroupConfigType entity type.
+    $bundle_entity_type = $this->entityTypeManager->getDefinition('group_config_type');
+    $build['#cache']['tags'] = $bundle_entity_type->getListCacheTags();
+
+    return $build;
+  }
+
+  /**
+   * Retrieves a list of available bundles for the add page.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param bool $create_mode
+   *   Whether the target entity still needs to be created.
+   *
+   * @return array
+   *   An array of group config type IDs, keyed by the plugin that was used to
+   *   generate their respective group config types.
+   *
+   * @see ::addPage()
+   */
+  protected function addPageBundles(GroupInterface $group, $create_mode) {
+    $bundles = [];
+
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = $this->entityTypeManager->getStorage('group_config_type');
+    foreach ($storage->loadByGroupType($group->getGroupType()) as $bundle => $group_config_type) {
+      // Skip the bundle if we are listing bundles that allow you to create an
+      // entity in the group and the bundle's plugin does not support that.
+      if ($create_mode && !$group_config_type->getConfigPlugin()->definesEntityAccess()) {
+        continue;
+      }
+
+      $bundles[$group_config_type->getConfigPluginId()] = $bundle;
+    }
+
+    return $bundles;
+  }
+
+  /**
+   * Returns the 'add_bundle_message' string for the add page.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param bool $create_mode
+   *   Whether the target entity still needs to be created.
+   *
+   * @return string|false
+   *   The translated string or FALSE if no string should be set.
+   *
+   * @see ::addPage()
+   */
+  protected function addPageBundleMessage(GroupInterface $group, $create_mode) {
+    // We do not set the 'add_bundle_message' variable because we deny access to
+    // the page if no bundle is available. This method exists so that modules
+    // that extend this controller may specify a message should they decide to
+    // allow access to their page even if it has no bundles.
+    return FALSE;
+  }
+
+  /**
+   * Returns the route name of the form the add page should link to.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param bool $create_mode
+   *   Whether the target entity still needs to be created.
+   *
+   * @return string
+   *   The route name.
+   *
+   * @see ::addPage()
+   */
+  protected function addPageFormRoute(GroupInterface $group, $create_mode) {
+    return $create_mode
+      ? 'entity.group_config.create_form'
+      : 'entity.group_config.add_form';
+  }
+
+  /**
+   * Provides the group config submission form.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param string $plugin_id
+   *   The group config enabler to add config with.
+   *
+   * @return array
+   *   A group submission form.
+   */
+  public function addForm(GroupInterface $group, $plugin_id) {
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+
+    $values = [
+      'type' => $plugin->getConfigTypeConfigId(),
+      'gid' => $group->id(),
+    ];
+    $group_config = $this->entityTypeManager()->getStorage('group_config')->create($values);
+
+    return $this->entityFormBuilder->getForm($group_config, 'add');
+  }
+
+  /**
+   * The _title_callback for the entity.group_config.add_form route.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param string $plugin_id
+   *   The group config enabler to add config with.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function addFormTitle(GroupInterface $group, $plugin_id) {
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+    $group_config_type = GroupConfigType::load($plugin->getConfigTypeConfigId());
+    return $this->t('Create @name', ['@name' => $group_config_type->label()]);
+  }
+
+  /**
+   * The _title_callback for the entity.group_config.edit_form route.
+   *
+   * Overrides the Drupal\Core\Entity\Controller\EntityController::editTitle().
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param \Drupal\Core\Entity\EntityInterface $_entity
+   *   (optional) An entity, passed in directly from the request attributes.
+   *
+   * @return string|null
+   *   The title for the entity edit page, if an entity was found.
+   */
+  public function editFormTitle(RouteMatchInterface $route_match, EntityInterface $_entity = NULL) {
+    if ($entity = $route_match->getParameter('group_config')) {
+      return $this->t('Edit %label', ['%label' => $entity->label()]);
+    }
+  }
+
+  /**
+   * The _title_callback for the entity.group_config.collection route.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   *
+   * @return string
+   *   The page title.
+   *
+   * @todo Revisit when 8.2.0 is released, https://www.drupal.org/node/2767853.
+   */
+  public function collectionTitle(GroupInterface $group) {
+    return $this->t('Related entities for @group', ['@group' => $group->label()]);
+  }
+
+  /**
+   * Provides the group config creation form.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the group config to.
+   * @param string $plugin_id
+   *   The group config enabler to add config with.
+   *
+   * @return array
+   *   A group config creation form.
+   */
+  public function createForm(GroupInterface $group, $plugin_id) {
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+
+    $wizard_id = 'group_entity';
+    $store = $this->privateTempStoreFactory->get($wizard_id);
+    $store_id = $plugin_id . ':' . $group->id();
+
+    // See if the plugin uses a wizard for creating new entities. Also pass this
+    // info to the form state.
+    $config = $plugin->getConfiguration();
+    $extra['group_wizard'] = $config['use_creation_wizard'];
+    $extra['group_wizard_id'] = $wizard_id;
+
+    // Pass the group, plugin ID and store ID to the form state as well.
+    $extra['group'] = $group;
+    $extra['group_config_enabler'] = $plugin_id;
+    $extra['store_id'] = $store_id;
+
+    // See if we are on the second step of the form.
+    $step2 = $extra['group_wizard'] && $store->get("$store_id:step") === 2;
+
+    // Config entity form, potentially as wizard step 1.
+    if (!$step2) {
+      // Figure out what entity type the plugin is serving.
+      $entity_type_id = $plugin->getEntityTypeId();
+      $entity_type = $this->entityTypeManager()->getDefinition($entity_type_id);
+      $storage = $this->entityTypeManager()->getStorage($entity_type_id);
+
+      // Only create a new entity if we have nothing stored.
+      if (!$entity = $store->get("$store_id:entity")) {
+        $values = [];
+        if (($key = $entity_type->getKey('bundle')) && ($bundle = $plugin->getEntityBundle())) {
+          $values[$key] = $bundle;
+        }
+        $entity = $storage->create($values);
+      }
+
+      // Use the add form handler if available.
+      $operation = 'default';
+      if ($entity_type->getFormClass('add')) {
+        $operation = 'add';
+      }
+    }
+    // Wizard step 2: Group config form.
+    else {
+      // Create an empty group config entity.
+      $values = [
+        'type' => $plugin->getConfigTypeConfigId(),
+        'gid' => $group->id(),
+      ];
+      $entity = $this->entityTypeManager()->getStorage('group_config')->create($values);
+
+      // Group config entities have an add form handler.
+      $operation = 'add';
+    }
+
+    // Return the entity form with the configuration gathered above.
+    return $this->entityFormBuilder()->getForm($entity, $operation, $extra);
+  }
+
+  /**
+   * The _title_callback for the entity.group_config.create_form route.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to create the group config in.
+   * @param string $plugin_id
+   *   The group config enabler to create config with.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function createFormTitle(GroupInterface $group, $plugin_id) {
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+    $group_config_type = GroupConfigType::load($plugin->getConfigTypeConfigId());
+    return $this->t('Create @name', ['@name' => $group_config_type->label()]);
+  }
+
+}
diff --git a/src/Entity/Controller/GroupConfigListBuilder.php b/src/Entity/Controller/GroupConfigListBuilder.php
new file mode 100644
index 0000000..f8b48c2
--- /dev/null
+++ b/src/Entity/Controller/GroupConfigListBuilder.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Drupal\group\Entity\Controller;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Routing\RedirectDestinationInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a list controller for group config entities.
+ *
+ * @ingroup group
+ */
+class GroupConfigListBuilder extends EntityListBuilder {
+
+  /**
+   * The group to show the config for.
+   *
+   * @var \Drupal\group\Entity\GroupInterface
+   */
+  protected $group;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Routing\RedirectDestinationInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The redirect destination.
+   *
+   * @var \Drupal\Core\Routing\RedirectDestinationInterface
+   */
+  protected $redirectDestination;
+
+  /**
+   * Constructs a new GroupConfigListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
+   *   The redirect destination.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, RedirectDestinationInterface $redirect_destination, RouteMatchInterface $route_match, EntityTypeInterface $entity_type) {
+    parent::__construct($entity_type, $entity_type_manager->getStorage($entity_type->id()));
+    $this->entityTypeManager = $entity_type_manager;
+    $this->redirectDestination = $redirect_destination;
+    // There should always be a group on the route for group config lists.
+    $this->group = $route_match->getParameters()->get('group');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('redirect.destination'),
+      $container->get('current_route_match'),
+      $entity_type
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityIds() {
+    $query = $this->getStorage()->getQuery();
+    $query->sort($this->entityType->getKey('id'));
+
+    // Only show group config for the group on the route.
+    $query->condition('gid', $this->group->id());
+
+    // Only add the pager if a limit is specified.
+    if ($this->limit) {
+      $query->pager($this->limit);
+    }
+
+    return $query->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = [
+      'id' => $this->t('ID'),
+      'label' => $this->t('Config label'),
+      'entity_type' => $this->t('Entity type'),
+      'plugin' => $this->t('Plugin used'),
+    ];
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\group\Entity\GroupConfigInterface $entity */
+    $row['id'] = $entity->id();
+
+    // EntityListBuilder sets the table rows using the #rows property, so we
+    // need to add links as render arrays using the 'data' key.
+    $row['label']['data'] = $entity->toLink()->toRenderable();
+    $entity_type_id = $entity->getConfigPlugin()->getEntityTypeId();
+    $row['entity_type'] = $this->entityTypeManager->getDefinition($entity_type_id)->getLabel();
+    $row['plugin'] = $entity->getConfigPlugin()->getLabel();
+
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('There are no config entities related to this group yet.');
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultOperations(EntityInterface $entity) {
+    /** @var \Drupal\group\Entity\GroupConfigInterface $entity */
+    $operations = parent::getDefaultOperations($entity);
+
+    // Improve the edit and delete operation labels.
+    if (isset($operations['edit'])) {
+      $operations['edit']['title'] = $this->t('Edit relation');
+    }
+    if (isset($operations['delete'])) {
+      $operations['delete']['title'] = $this->t('Delete relation');
+    }
+
+    // Slap on redirect destinations for the administrative operations.
+    $destination = $this->redirectDestination->getAsArray();
+    foreach ($operations as $key => $operation) {
+      $operations[$key]['query'] = $destination;
+    }
+
+    // Add an operation to view the actual entity.
+    if ($entity->getEntity()->access('view') && $entity->getEntity()->hasLinkTemplate('canonical')) {
+      $operations['view'] = [
+        'title' => $this->t('View related entity'),
+        'weight' => 101,
+        'url' => $entity->getEntity()->toUrl('canonical'),
+      ];
+    }
+
+    return $operations;
+  }
+
+}
diff --git a/src/Entity/Controller/GroupTypeController.php b/src/Entity/Controller/GroupTypeController.php
index 54d6a82..685b669 100644
--- a/src/Entity/Controller/GroupTypeController.php
+++ b/src/Entity/Controller/GroupTypeController.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\group\Entity\Controller;
 
+use Drupal\group\Entity\GroupConfigType;
 use Drupal\group\Entity\GroupTypeInterface;
 use Drupal\group\Entity\GroupContentType;
+use Drupal\group\Plugin\GroupConfigEnablerInterface;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
 use Drupal\group\Plugin\GroupContentEnablerInterface;
 use Drupal\group\Plugin\GroupContentEnablerManagerInterface;
 use Drupal\Core\Url;
@@ -32,6 +35,15 @@ class GroupTypeController extends ControllerBase {
   protected $pluginManager;
 
   /**
+   * Fig
+   * The group content plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+
+  protected $configPluginManager;
+
+  /**
    * The module manager.
    *
    * @var \Drupal\Core\Extension\ModuleHandlerInterface
@@ -54,11 +66,14 @@ class GroupTypeController extends ControllerBase {
    *   The entity type manager.
    * @param \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager
    *   The group content plugin manager.
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $config_plugin_manager
+   *   The group config plugin manager.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, GroupContentEnablerManagerInterface $plugin_manager) {
+  public function __construct(ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, GroupContentEnablerManagerInterface $plugin_manager, GroupConfigEnablerManagerInterface $config_plugin_manager) {
     $this->moduleHandler = $module_handler;
     $this->entityTypeManager = $entity_type_manager;
     $this->pluginManager = $plugin_manager;
+    $this->configPluginManager = $config_plugin_manager;
   }
 
   /**
@@ -68,7 +83,8 @@ class GroupTypeController extends ControllerBase {
     return new static(
       $container->get('module_handler'),
       $container->get('entity_type.manager'),
-      $container->get('plugin.manager.group_content_enabler')
+      $container->get('plugin.manager.group_content_enabler'),
+      $container->get('plugin.manager.group_config_enabler')
     );
   }
 
@@ -99,7 +115,7 @@ class GroupTypeController extends ControllerBase {
         'status' => $this->t('Status'),
         'operations' => $this->t('Operations'),
       ],
-      '#suffix' =>  $this->t('<em>* These plugins are set to be always on by their providing module.</em>'),
+      '#suffix' => $this->t('<em>* These plugins are set to be always on by their providing module.</em>'),
     ];
 
     $installed = $this->pluginManager->getInstalledIds($group_type);
@@ -117,6 +133,50 @@ class GroupTypeController extends ControllerBase {
   }
 
   /**
+   * Builds an admin interface to manage the group type's group content plugins.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   The group type to build an interface for.
+   *
+   * @return array
+   *   The render array for the page.
+   */
+  public function config_entity(GroupTypeInterface $group_type) {
+    $this->groupType = $group_type;
+
+    // Render the table of available content enablers.
+    $page['system_compact_link'] = [
+      '#id' => FALSE,
+      '#type' => 'system_compact_link',
+    ];
+
+    $page['content'] = [
+      '#type' => 'table',
+      '#header' => [
+        'info' => $this->t('Plugin information'),
+        'provider' => $this->t('Provided by'),
+        'entity_type_id' => $this->t('Applies to'),
+        'status' => $this->t('Status'),
+        'operations' => $this->t('Operations'),
+      ],
+      '#suffix' => $this->t('<em>* These plugins are set to be always on by their providing module.</em>'),
+    ];
+
+    $installed = $this->configPluginManager->getInstalledIds($group_type);
+    foreach ($this->configPluginManager->getAll() as $plugin_id => $plugin) {
+      // If the plugin is installed on the group type, use that one instead of
+      // an 'empty' version so that we may use methods on it which expect to
+      // have a group type configured.
+      if (in_array($plugin_id, $installed)) {
+        $plugin = $this->groupType->getConfigPlugin($plugin_id);
+      }
+      $page['content'][$plugin_id] = $this->buildConfigRow($plugin);
+    }
+
+    return $page;
+  }
+
+  /**
    * Builds a row for a content enabler plugin.
    *
    * @param \Drupal\group\Plugin\GroupContentEnablerInterface $plugin
@@ -150,10 +210,61 @@ class GroupTypeController extends ControllerBase {
         ],
       ],
       'provider' => [
-        '#markup' => $this->moduleHandler->getName($plugin->getProvider())
+        '#markup' => $this->moduleHandler->getName($plugin->getProvider()),
+      ],
+      'entity_type_id' => [
+        '#markup' => $this->entityTypeManager->getDefinition($plugin->getEntityTypeId())->getLabel(),
+      ],
+      'status' => ['#markup' => $status],
+      'operations' => $this->buildOperations($plugin),
+    ];
+
+    // Show the content enabler description if toggled on.
+    if (!system_admin_compact_mode()) {
+      $row['info']['#context']['description'] = $plugin->getDescription();
+    }
+
+    return $row;
+  }
+
+  /**
+   * Builds a row for a content enabler plugin.
+   *
+   * @param \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin
+   *   The content enabler plugin to build operation links for.
+   *
+   * @return array
+   *   A render array to use as a table row.
+   */
+  public function buildConfigRow(GroupConfigEnablerInterface $plugin) {
+    $installed = $this->configPluginManager->getInstalledIds($this->groupType);
+
+    // Get the plugin status.
+    if (in_array($plugin->getPluginId(), $installed)) {
+      $status = $this->t('Installed');
+
+      // Mark enforced plugins with an asterisk.
+      if ($plugin->isEnforced()) {
+        $status .= '*';
+      }
+    }
+    else {
+      $status = $this->t('Not installed');
+    }
+
+    $row = [
+      'info' => [
+        '#type' => 'inline_template',
+        '#template' => '<div class="description"><span class="label">{{ label }}</span>{% if description %}<br/>{{ description }}{% endif %}</div>',
+        '#context' => [
+          'label' => $plugin->getLabel(),
+        ],
+      ],
+      'provider' => [
+        '#markup' => $this->moduleHandler->getName($plugin->getProvider()),
       ],
       'entity_type_id' => [
-        '#markup' => $this->entityTypeManager->getDefinition($plugin->getEntityTypeId())->getLabel()
+        '#markup' => $this->entityTypeManager->getDefinition($plugin->getEntityTypeId())->getLabel(),
       ],
       'status' => ['#markup' => $status],
       'operations' => $this->buildOperations($plugin),
@@ -181,7 +292,12 @@ class GroupTypeController extends ControllerBase {
    *   - weight: The weight of this operation.
    */
   public function getOperations($plugin) {
-    return $plugin->getOperations() + $this->getDefaultOperations($plugin);
+    if ($plugin instanceof GroupContentEnablerInterface) {
+      return $plugin->getOperations() + $this->getContentDefaultOperations($plugin);
+    }
+    if ($plugin instanceof GroupConfigEnablerInterface) {
+      return $plugin->getOperations() + $this->getConfigDefaultOperations($plugin);
+    }
   }
 
   /**
@@ -194,7 +310,7 @@ class GroupTypeController extends ControllerBase {
    *   The array structure is identical to the return value of
    *   self::getOperations().
    */
-  protected function getDefaultOperations($plugin) {
+  protected function getContentDefaultOperations($plugin) {
     $operations = [];
 
     $plugin_id = $plugin->getPluginId();
@@ -237,6 +353,58 @@ class GroupTypeController extends ControllerBase {
   }
 
   /**
+   * Gets the group type's config plugin's default operation links.
+   *
+   * @param \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin
+   *   The content enabler plugin to build operation links for.
+   *
+   * @return array
+   *   The array structure is identical to the return value of
+   *   self::getOperations().
+   */
+  protected function getConfigDefaultOperations($plugin) {
+    $operations = [];
+
+    $plugin_id = $plugin->getPluginId();
+    $installed = $this->configPluginManager->getInstalledIds($this->groupType);
+
+    if (in_array($plugin_id, $installed)) {
+      /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+      $group_config_type_id = $plugin->getConfigTypeConfigId();
+      $group_config_type = GroupConfigType::load($group_config_type_id);
+
+      $route_params = [
+        'group_config_type' => $group_config_type_id,
+      ];
+
+      $operations['configure'] = [
+        'title' => $this->t('Configure'),
+        'url' => new Url('entity.group_config_type.edit_form', $route_params),
+      ];
+
+      if (!$plugin->isEnforced()) {
+        $operations['uninstall'] = [
+          'title' => $this->t('Uninstall'),
+          'weight' => 99,
+          'url' => new Url('entity.group_config_type.delete_form', $route_params),
+        ];
+      }
+
+      if ($this->moduleHandler->moduleExists('field_ui')) {
+        $operations += field_ui_entity_operation($group_config_type);
+      }
+    }
+    elseif (!$plugin->isEnforced()) {
+      $operations['install'] = [
+        'title' => $this->t('Install'),
+        'url' => new Url('entity.group_config_type.add_form', ['group_type' => $this->groupType->id(), 'plugin_id' => $plugin_id]),
+      ];
+    }
+
+    return $operations;
+  }
+
+  /**
    * Builds operation links for the group type's content plugins.
    *
    * @param \Drupal\group\Plugin\GroupContentEnablerInterface $plugin
diff --git a/src/Entity/Form/GroupConfigDeleteForm.php b/src/Entity/Form/GroupConfigDeleteForm.php
new file mode 100644
index 0000000..5631003
--- /dev/null
+++ b/src/Entity/Form/GroupConfigDeleteForm.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\group\Entity\Form;
+
+use Drupal\Core\Entity\ContentEntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a form for deleting a group content entity.
+ */
+class GroupConfigDeleteForm extends ContentEntityConfirmFormBase {
+
+  /**
+   * Returns the plugin responsible for this piece of group config.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   *   The responsible group config enabler plugin.
+   */
+  protected function getConfigPlugin() {
+    /** @var \Drupal\group\Entity\GroupConfig $group_config */
+    $group_config = $this->getEntity();
+    return $group_config->getConfigPlugin();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelURL() {
+    /** @var \Drupal\group\Entity\GroupConfig $group_config */
+    $group_config = $this->getEntity();
+    $group = $group_config->getGroup();
+    $route_params = [
+      'group' => $group->id(),
+      'group_config' => $group_config->id(),
+    ];
+    return new Url('entity.group_config.canonical', $route_params);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\group\Entity\GroupConfig $group_config */
+    $group_config = $this->getEntity();
+    $group = $group_config->getGroup();
+    $group_config->delete();
+
+    \Drupal::logger('group_config')->notice('@type: deleted %title.', [
+      '@type' => $group_config->bundle(),
+      '%title' => $group_config->label(),
+    ]);
+
+    $form_state->setRedirect('entity.group.canonical', ['group' => $group->id()]);
+  }
+
+}
diff --git a/src/Entity/Form/GroupConfigForm.php b/src/Entity/Form/GroupConfigForm.php
new file mode 100644
index 0000000..1f43e6a
--- /dev/null
+++ b/src/Entity/Form/GroupConfigForm.php
@@ -0,0 +1,209 @@
+<?php
+
+namespace Drupal\group\Entity\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form controller for the group config edit forms.
+ *
+ * @ingroup group
+ */
+class GroupConfigForm extends ContentEntityForm {
+
+  /**
+   * The private store factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $privateTempStoreFactory;
+
+  /**
+   * Constructs a GroupConfigForm object.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   *   The private store factory.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityManagerInterface $entity_manager) {
+    $this->privateTempStoreFactory = $temp_store_factory;
+    parent::__construct($entity_manager);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('entity.manager')
+    );
+  }
+
+  /**
+   * Returns the plugin responsible for this piece of group config.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   *   The responsible group config enabler plugin.
+   */
+  protected function getConfigPlugin() {
+    /** @var \Drupal\group\Entity\GroupConfig $group_config */
+    $group_config = $this->getEntity();
+    return $group_config->getConfigPlugin();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Do not allow to edit the group config subject through the UI. Also hide
+    // the field when we are on step 2 of a creation wizard.
+    if ($this->operation !== 'add' || $form_state->get('group_wizard')) {
+      $form['entity_id']['#access'] = FALSE;
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+
+    // If we are on step 2 of a wizard, we need to alter the actions.
+    if ($form_state->get('group_wizard')) {
+      $wizard_id = $form_state->get('group_wizard_id');
+      $store = $this->privateTempStoreFactory->get($wizard_id);
+      $store_id = $form_state->get('store_id');
+
+      if ($store->get("$store_id:step") === 2) {
+        // Add a back button to return to step 1 with.
+        $actions['back'] = [
+          '#type' => 'submit',
+          '#value' => $this->t('Back'),
+          '#submit' => ['::back'],
+          '#limit_validation_errors' => [],
+        ];
+
+        // Make the label of the save button more intuitive.
+        if ($wizard_id == 'group_creator') {
+          $actions['submit']['#value'] = $this->t('Save group and membership');
+        }
+        elseif ($wizard_id == 'group_entity') {
+          $entity_type_id = $store->get("$store_id:entity")->getEntityTypeId();
+          $entity_type = $this->entityManager->getDefinition($entity_type_id);
+          $replace = [
+            '@entity_type' => $entity_type->getLowercaseLabel(),
+            '@group' => $this->getEntity()->getGroup()->label(),
+          ];
+          $actions['submit']['#value'] = $this->t('Create @entity_type in @group', $replace);
+        }
+
+        // Make sure we complete the wizard before saving the group config.
+        $index = array_search('::save', $actions['submit']['#submit']);
+        array_splice($actions['submit']['#submit'], $index, 0, '::complete');
+      }
+    }
+
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $return = parent::save($form, $form_state);
+
+    /** @var \Drupal\group\Entity\GroupConfigInterface $group_config */
+    $group_config = $this->getEntity();
+
+    // The below redirect ensures the user will be redirected to something they
+    // can view in the following order: The relationship entity (group config),
+    // they target entity itself, the group and finally the front page. This
+    // only applies if there was no destination GET parameter set in the URL.
+    if ($group_config->access('view')) {
+      $form_state->setRedirectUrl($group_config->toUrl());
+    }
+    elseif ($group_config->getEntity()->access('view')) {
+      $form_state->setRedirectUrl($group_config->getEntity()->toUrl());
+    }
+    elseif ($group_config->getGroup()->access('view')) {
+      $form_state->setRedirectUrl($group_config->getGroup()->toUrl());
+    }
+    else {
+      $form_state->setRedirect('<front>');
+    }
+
+    return $return;
+  }
+
+  /**
+   * Goes back to step 1 of the creation wizard.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\group\Entity\Controller\GroupConfigController::createForm()
+   */
+  public function back(array &$form, FormStateInterface $form_state) {
+    $store = $this->privateTempStoreFactory->get($form_state->get('group_wizard_id'));
+    $store_id = $form_state->get('store_id');
+    $store->set("$store_id:step", 1);
+
+    // Disable any URL-based redirect when going back to the previous step.
+    $request = $this->getRequest();
+    $form_state->setRedirect('<current>', [], ['query' => $request->query->all()]);
+    $request->query->remove('destination');
+  }
+
+  /**
+   * Completes the creation wizard by saving the target entity.
+   *
+   * Please note that we are instantiating an entity form to replicate the first
+   * step and call the save method on that form. This is done to ensure that any
+   * logic in the save handler is actually run when the wizard completes.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\group\Entity\Controller\GroupConfigController::createForm()
+   */
+  public function complete(array &$form, FormStateInterface $form_state) {
+    $wizard_id = $form_state->get('group_wizard_id');
+    $store = $this->privateTempStoreFactory->get($wizard_id);
+    $store_id = $form_state->get('store_id');
+    $entity = $store->get("$store_id:entity");
+
+    // Use the add form handler, if available, otherwise default.
+    $operation = 'default';
+    if ($entity->getEntityType()->getFormClass('add')) {
+      $operation = 'add';
+    }
+
+    // Replicate the form from step 1 and call the save method.
+    $form_object = $this->entityManager->getFormObject($entity->getEntityTypeId(), $operation);
+    $form_object->setEntity($entity);
+    $form_object->save($form, $form_state);
+
+    // Add the newly saved entity's ID to the group config entity.
+    $property = $wizard_id == 'group_creator' ? 'gid' : 'entity_id';
+    $this->entity->set($property, $entity->id());
+
+    // We also clear the temp store so we can start fresh next time around.
+    $store->delete("$store_id:step");
+    $store->delete("$store_id:entity");
+  }
+
+}
diff --git a/src/Entity/Form/GroupConfigTypeDeleteForm.php b/src/Entity/Form/GroupConfigTypeDeleteForm.php
new file mode 100644
index 0000000..bfabb5a
--- /dev/null
+++ b/src/Entity/Form/GroupConfigTypeDeleteForm.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\group\Entity\Form;
+
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Entity\EntityDeleteForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for group config type deletion.
+ *
+ * Instead of just deleting the group config type here, we use this form as a
+ * mean of uninstalling a group config enabler plugin which will actually
+ * trigger the deletion of the group config type.
+ */
+class GroupConfigTypeDeleteForm extends EntityDeleteForm {
+
+  /**
+   * The query factory to create entity queries.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $queryFactory;
+
+  /**
+   * Constructs a new GroupConfigTypeDeleteForm object.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
+   *   The entity query object.
+   */
+  public function __construct(QueryFactory $query_factory) {
+    $this->queryFactory = $query_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    return $this->t('Are you sure you want to uninstall the %plugin plugin?', ['%plugin' => $group_config_type->getConfigPlugin()->getLabel()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    return Url::fromRoute('entity.group_type.config_plugins', ['group_type' => $group_config_type->getGroupTypeId()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    $plugin = $group_config_type->getConfigPlugin();
+    $replace = [
+      '%entity_type' => $this->entityTypeManager->getDefinition($plugin->getEntityTypeId())->getLabel(),
+      '%group_type' => $group_config_type->getGroupType()->label(),
+    ];
+    return $this->t('You will no longer be able to add %entity_type entities to %group_type groups.', $replace);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Uninstall');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $entity_count = $this->queryFactory->get('group_config')
+      ->condition('type', $this->entity->id())
+      ->count()
+      ->execute();
+
+    if (!empty($entity_count)) {
+      $form['#title'] = $this->getQuestion();
+      $form['description'] = [
+        '#markup' => '<p>' . $this->t('You can not uninstall this config plugin until you have removed all of the config that uses it.') . '</p>',
+      ];
+
+      return $form;
+    }
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    $group_type = $group_config_type->getGroupType();
+    $plugin = $group_config_type->getConfigPlugin();
+
+    $group_config_type->delete();
+    \Drupal::logger('group_config_type')->notice('Uninstalled %plugin from %group_type.', [
+      '%plugin' => $plugin->getLabel(),
+      '%group_type' => $group_type->label(),
+    ]);
+
+    $form_state->setRedirect('entity.group_type.config_plugins', ['group_type' => $group_type->id()]);
+    drupal_set_message($this->t('The config plugin was uninstalled from the group type.'));
+  }
+
+}
diff --git a/src/Entity/Form/GroupConfigTypeForm.php b/src/Entity/Form/GroupConfigTypeForm.php
new file mode 100644
index 0000000..3042b74
--- /dev/null
+++ b/src/Entity/Form/GroupConfigTypeForm.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\group\Entity\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form controller for group config type forms.
+ */
+class GroupConfigTypeForm extends EntityForm {
+
+  /**
+   * The group config enabler plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a new GroupConfigTypeForm.
+   *
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config plugin manager.
+   */
+  public function __construct(GroupConfigEnablerManagerInterface $plugin_manager) {
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.group_config_enabler')
+    );
+  }
+
+  /**
+   * Returns the configurable plugin for the group config type.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   *   The configurable group config enabler plugin.
+   */
+  protected function getConfigPlugin() {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    $group_type = $group_config_type->getGroupType();
+
+    // Initialize an empty plugin so we can show a default configuration form.
+    if ($this->operation == 'add') {
+      $plugin_id = $group_config_type->getConfigPluginId();
+      $configuration['group_type_id'] = $group_type->id();
+      return $this->pluginManager->createInstance($plugin_id, $configuration);
+    }
+    // Return the already configured plugin for existing group config types.
+    else {
+      return $group_config_type->getConfigPlugin();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    $group_type = $group_config_type->getGroupType();
+    $plugin = $this->getConfigPlugin();
+
+    // @todo These messages may need some love.
+    if ($this->operation == 'add') {
+      $form['#title'] = $this->t('Install config plugin');
+      $message = 'By installing the %plugin plugin, you will allow %entity_type entities to be added to groups of type %group_type';
+    }
+    else {
+      $form['#title'] = $this->t('Configure config plugin');
+      $message = 'This form allows you to configure the %plugin plugin for the %group_type group type.';
+    }
+
+    // Add in the replacements for the $message variable set above.
+    $replace = [
+      '%plugin' => $plugin->getLabel(),
+      '%entity_type' => $this->entityTypeManager->getDefinition($plugin->getEntityTypeId())->getLabel(),
+      '%group_type' => $group_type->label(),
+    ];
+
+    // Display a description to explain the purpose of the form.
+    $form['description'] = [
+      '#markup' => $this->t($message, $replace),
+    ];
+
+    // Add in the plugin configuration form.
+    $form += $plugin->buildConfigurationForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->operation == 'add' ? $this->t('Install plugin') : $this->t('Save configuration'),
+      '#submit' => ['::submitForm'],
+    ];
+
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $plugin = $this->getConfigPlugin();
+    $plugin->validateConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\group\Entity\GroupConfigTypeInterface $group_config_type */
+    $group_config_type = $this->getEntity();
+    $group_type = $group_config_type->getGroupType();
+    $plugin = $this->getConfigPlugin();
+    $plugin->submitConfigurationForm($form, $form_state);
+
+    // Remove button and internal Form API values from submitted values.
+    $form_state->cleanValues();
+
+    // Extract the values as configuration that should be saved.
+    $config = $form_state->getValues();
+
+    // If we are on an 'add' form, we create the group config type using the
+    // plugin configuration submitted using this form.
+    if ($this->operation == 'add') {
+      /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+      $storage = $this->entityTypeManager->getStorage('group_config_type');
+      $storage->createFromPlugin($group_type, $plugin->getPluginId(), $config)->save();
+      drupal_set_message($this->t('The config plugin was installed on the group type.'));
+    }
+    // Otherwise, we update the existing group config type's configuration.
+    else {
+      $group_config_type->updateConfigPlugin($config);
+      drupal_set_message($this->t('The config plugin configuration was saved.'));
+    }
+
+    $form_state->setRedirect('entity.group_type.config_plugins', ['group_type' => $group_type->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
+    if ($route_match->getRawParameter($entity_type_id) !== NULL) {
+      return $route_match->getParameter($entity_type_id);
+    }
+
+    // If we are on the create form, we can't extract an entity from the route,
+    // so we need to create one based on the route parameters.
+    $values = [];
+    if ($route_match->getRawParameter('group_type') !== NULL && $route_match->getRawParameter('plugin_id') !== NULL) {
+      $values = [
+        'group_type' => $route_match->getRawParameter('group_type'),
+        'config_plugin' => $route_match->getRawParameter('plugin_id'),
+      ];
+    }
+    return $this->entityTypeManager->getStorage($entity_type_id)->create($values);
+  }
+
+}
diff --git a/src/Entity/GroupConfig.php b/src/Entity/GroupConfig.php
new file mode 100644
index 0000000..ebc5900
--- /dev/null
+++ b/src/Entity/GroupConfig.php
@@ -0,0 +1,359 @@
+<?php
+
+namespace Drupal\group\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the Group content entity.
+ *
+ * @ingroup group
+ *
+ * @ContentEntityType(
+ *   id = "group_config",
+ *   label = @Translation("Group config"),
+ *   label_singular = @Translation("group config item"),
+ *   label_plural = @Translation("group config items"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count group config item",
+ *     plural = "@count group config items"
+ *   ),
+ *   bundle_label = @Translation("Group config type"),
+ *   handlers = {
+ *     "storage" = "Drupal\group\Entity\Storage\GroupConfigStorage",
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "views_data" = "Drupal\group\Entity\Views\GroupConfigViewsData",
+ *     "list_builder" = "Drupal\group\Entity\Controller\GroupConfigListBuilder",
+ *     "route_provider" = {
+ *       "html" = "Drupal\group\Entity\Routing\GroupConfigRouteProvider",
+ *     },
+ *     "form" = {
+ *       "add" = "Drupal\group\Entity\Form\GroupConfigForm",
+ *       "edit" = "Drupal\group\Entity\Form\GroupConfigForm",
+ *       "delete" = "Drupal\group\Entity\Form\GroupConfigDeleteForm",
+ *       "group-join" = "Drupal\group\Form\GroupConfigJoinForm",
+ *       "group-leave" = "Drupal\group\Form\GroupConfigLeaveForm",
+ *     },
+ *     "access" = "Drupal\group\Entity\Access\GroupConfigAccessControlHandler",
+ *   },
+ *   base_table = "group_config",
+ *   data_table = "group_config_field_data",
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "langcode" = "langcode",
+ *     "bundle" = "type",
+ *     "label" = "label"
+ *   },
+ *   links = {
+ *     "add-form" = "/group/{group}/config/add/{plugin_id}",
+ *     "add-page" = "/group/{group}/config/add",
+ *     "canonical" = "/group/{group}/config/{group_config}",
+ *     "collection" = "/group/{group}/config",
+ *     "create-form" = "/group/{group}/config/create/{plugin_id}",
+ *     "create-page" = "/group/{group}/config/create",
+ *     "delete-form" = "/group/{group}/config/{group_config}/delete",
+ *     "edit-form" = "/group/{group}/config/{group_config}/edit"
+ *   },
+ *   bundle_entity_type = "group_config_type",
+ *   field_ui_base_route = "entity.group_config_type.edit_form",
+ *   permission_granularity = "bundle",
+ *   constraints = {
+ *     "GroupConfigCardinality" = {}
+ *   }
+ * )
+ */
+class GroupConfig extends ContentEntityBase implements GroupConfigInterface {
+
+  use EntityChangedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupConfigType() {
+    return $this->type->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroup() {
+    return $this->gid->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntity() {
+    return $this->entity_id->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigPlugin() {
+    return $this->getGroupConfigType()->getConfigPlugin();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function loadByConfigPluginId($plugin_id) {
+    /** @var \Drupal\group\Entity\Storage\GroupConfigStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage('group_config');
+    return $storage->loadByConfigPluginId($plugin_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function loadByEntity(ContentEntityInterface $entity) {
+    /** @var \Drupal\group\Entity\Storage\GroupConfigStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage('group_config');
+    return $storage->loadByEntity($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    return $this->getConfigPlugin()->getConfigLabel($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function urlRouteParameters($rel) {
+    $uri_route_parameters = parent::urlRouteParameters($rel);
+    $uri_route_parameters['group'] = $this->getGroup()->id();
+    // These routes depend on the plugin ID.
+    if (in_array($rel, ['add-form', 'create-form'])) {
+      $uri_route_parameters['plugin_id'] = $this->getConfigPlugin()->getPluginId();
+    }
+    return $uri_route_parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedTime() {
+    return $this->get('changed')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('uid')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('uid')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('uid', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    // Set the label so the DB also reflects it.
+    $this->set('label', $this->label());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    if ($update === FALSE) {
+      // We want to make sure that the entity we just added to the group behaves
+      // as a grouped entity. This means we may need to update access records,
+      // flush some caches containing the entity or perform other operations we
+      // cannot possibly know about. Lucky for us, all of that behavior usually
+      // happens when saving an entity so let's re-save the added entity.
+      $this->getEntity()->save();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageInterface $storage, array $entities) {
+    parent::postDelete($storage, $entities);
+
+    // For the same reasons we re-save entities that are added to a group, we
+    // need to re-save entities that were removed from one. See ::postSave().
+    /** @var GroupConfigInterface[] $entities */
+    foreach ($entities as $group_config) {
+      // We only save the entity if it still exists to avoid trying to save an
+      // entity that just got deleted and triggered the deletion of its group
+      // config entities.
+      if ($entity = $group_config->getEntity()) {
+        // @todo Revisit when https://www.drupal.org/node/2754399 lands.
+        $entity->save();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['gid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Parent group'))
+      ->setDescription(t('The group containing the entity.'))
+      ->setSetting('target_type', 'group')
+      ->setReadOnly(TRUE);
+
+    // Borrowed this logic from the Comment module.
+    // Warning! May change in the future: https://www.drupal.org/node/2346347
+    $fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Config'))
+      ->setDescription(t('The entity to add to the group.'))
+      ->setSetting('target_type', 'group_config_type')
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'placeholder' => '',
+        ],
+      ])
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayConfigurable('form', TRUE)
+      ->setRequired(TRUE);
+
+    $fields['label'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Title'))
+      ->setReadOnly(TRUE)
+      ->setTranslatable(TRUE)
+      ->setSetting('max_length', 255)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'string',
+        'weight' => -5,
+      ]);
+
+    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Group config creator'))
+      ->setDescription(t('The username of the group config creator.'))
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setDefaultValueCallback('Drupal\group\Entity\GroupConfig::getCurrentUserId')
+      ->setTranslatable(TRUE)
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayConfigurable('form', TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created on'))
+      ->setDescription(t('The time that the group config was created.'))
+      ->setTranslatable(TRUE);
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed on'))
+      ->setDescription(t('The time that the group config was last edited.'))
+      ->setTranslatable(TRUE);
+
+    if (\Drupal::moduleHandler()->moduleExists('path')) {
+      $fields['path'] = BaseFieldDefinition::create('path')
+        ->setLabel(t('URL alias'))
+        ->setTranslatable(TRUE)
+        ->setDisplayOptions('form', [
+          'type' => 'path',
+          'weight' => 30,
+        ])
+        ->setDisplayConfigurable('form', TRUE)
+        ->setComputed(TRUE);
+    }
+
+    return $fields;
+  }
+
+  /**
+   * Default value callback for 'uid' base field definition.
+   *
+   * @see ::baseFieldDefinitions()
+   *
+   * @return array
+   *   An array of default values.
+   */
+  public static function getCurrentUserId() {
+    return [\Drupal::currentUser()->id()];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    // Borrowed this logic from the Comment module.
+    // Warning! May change in the future: https://www.drupal.org/node/2346347
+    if ($group_config_type = GroupConfigType::load($bundle)) {
+      $plugin = $group_config_type->getConfigPlugin();
+
+      /** @var \Drupal\Core\Field\BaseFieldDefinition $original */
+      $original = $base_field_definitions['entity_id'];
+
+      // Recreated the original entity_id field so that it does not contain any
+      // data in its "propertyDefinitions" or "schema" properties because those
+      // were set based on the base field which had no clue what bundle to serve
+      // up until now. This is a bug in core because we can't simply unset those
+      // two properties, see: https://www.drupal.org/node/2346329
+      $fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
+        ->setLabel($plugin->getEntityReferenceLabel() ?: $original->getLabel())
+        ->setDescription($plugin->getEntityReferenceDescription() ?: $original->getDescription())
+        ->setConstraints($original->getConstraints())
+        ->setDisplayOptions('view', $original->getDisplayOptions('view'))
+        ->setDisplayOptions('form', $original->getDisplayOptions('form'))
+        ->setDisplayConfigurable('view', $original->isDisplayConfigurable('view'))
+        ->setDisplayConfigurable('form', $original->isDisplayConfigurable('form'))
+        ->setRequired($original->isRequired());
+
+      foreach ($plugin->getEntityReferenceSettings() as $name => $setting) {
+        $fields['entity_id']->setSetting($name, $setting);
+      }
+
+      return $fields;
+    }
+
+    return [];
+  }
+
+}
diff --git a/src/Entity/GroupConfigInterface.php b/src/Entity/GroupConfigInterface.php
new file mode 100644
index 0000000..774e1ca
--- /dev/null
+++ b/src/Entity/GroupConfigInterface.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\group\Entity;
+
+use Drupal\user\EntityOwnerInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+
+/**
+ * Provides an interface defining a Group config entity.
+ *
+ * @ingroup group
+ */
+interface GroupConfigInterface extends ContentEntityInterface, EntityOwnerInterface, EntityChangedInterface {
+
+  /**
+   * Returns the group config type entity the group config uses.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface
+   */
+  public function getGroupConfigType();
+
+  /**
+   * Returns the group the group config belongs to.
+   *
+   * @return \Drupal\group\Entity\GroupInterface
+   */
+  public function getGroup();
+
+  /**
+   * Returns the entity that was added as group config.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   */
+  public function getEntity();
+
+  /**
+   * Returns the config enabler plugin that handles the group config.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   */
+  public function getConfigPlugin();
+
+  /**
+   * Loads group config entities by their responsible plugin ID.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface[]
+   *   An array of group config entities indexed by their IDs.
+   */
+  public static function loadByConfigPluginId($plugin_id);
+
+  /**
+   * Loads group config entities which reference a given entity.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   An entity which may be within one or more groups.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface[]
+   *   An array of group config entities which reference the given entity.
+   */
+  public static function loadByEntity(ContentEntityInterface $entity);
+
+}
diff --git a/src/Entity/GroupConfigType.php b/src/Entity/GroupConfigType.php
new file mode 100644
index 0000000..dd00f37
--- /dev/null
+++ b/src/Entity/GroupConfigType.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace Drupal\group\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+
+/**
+ * Defines the Group config type configuration entity.
+ *
+ * @ConfigEntityType(
+ *   id = "group_config_type",
+ *   label = @Translation("Group config type"),
+ *   label_singular = @Translation("group config type"),
+ *   label_plural = @Translation("group config types"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count group config type",
+ *     plural = "@count group config types"
+ *   ),
+ *   handlers = {
+ *     "storage" = "Drupal\group\Entity\Storage\GroupConfigTypeStorage",
+ *     "access" = "Drupal\group\Entity\Access\GroupConfigTypeAccessControlHandler",
+ *     "form" = {
+ *       "add" = "Drupal\group\Entity\Form\GroupConfigTypeForm",
+ *       "edit" = "Drupal\group\Entity\Form\GroupConfigTypeForm",
+ *       "delete" = "Drupal\group\Entity\Form\GroupConfigTypeDeleteForm"
+ *     },
+ *   },
+ *   admin_permission = "administer group",
+ *   config_prefix = "config_type",
+ *   bundle_of = "group_config",
+ *   static_cache = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "description",
+ *     "group_type",
+ *     "config_plugin",
+ *     "plugin_config",
+ *   }
+ * )
+ */
+class GroupConfigType extends ConfigEntityBundleBase implements GroupConfigTypeInterface {
+
+  /**
+   * The machine name of the group config type.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The human-readable name of the group config type.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * A brief description of the group config type.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * The group type ID for the group config type.
+   *
+   * @var string
+   */
+  protected $group_type;
+
+  /**
+   * The group config enabler plugin ID for the group config type.
+   *
+   * @var string
+   */
+  protected $config_plugin;
+
+  /**
+   * The group config enabler plugin configuration for group config type.
+   *
+   * @var array
+   */
+  protected $plugin_config = [];
+
+  /**
+   * The config enabler plugin instance.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerInterface
+   */
+  protected $pluginInstance;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupType() {
+    return GroupType::load($this->getGroupTypeId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupTypeId() {
+    return $this->group_type;
+  }
+
+  /**
+   * Returns the config enabler plugin manager.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   *   The group config plugin manager.
+   */
+  protected function getConfigEnablerManager() {
+    return \Drupal::service('plugin.manager.group_config_enabler');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigPlugin() {
+    if (!isset($this->pluginInstance)) {
+      $configuration = $this->plugin_config;
+      $configuration['group_type_id'] = $this->getGroupTypeId();
+      $this->pluginInstance = $this->getConfigEnablerManager()->createInstance($this->getConfigPluginId(), $configuration);
+    }
+    return $this->pluginInstance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigPluginId() {
+    return $this->config_plugin;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateConfigPlugin(array $configuration) {
+    $this->plugin_config = $configuration;
+    $this->save();
+
+    // Make sure people get a fresh local plugin instance.
+    $this->pluginInstance = NULL;
+
+    // Make sure people get a freshly configured plugin collection.
+    $this->getConfigEnablerManager()->clearCachedGroupTypeCollections($this->getGroupType());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function loadByConfigPluginId($plugin_id) {
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage('group_config_type');
+    return $storage->loadByConfigPluginId($plugin_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function loadByEntityTypeId($entity_type_id) {
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = \Drupal::entityTypeManager()->getStorage('group_config_type');
+    return $storage->loadByEntityTypeId($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    if (!$update) {
+      // When a new GroupConfigType is saved, we clear the views data cache to
+      // make sure that all of the views data which relies on group config
+      // types is up to date.
+      if (\Drupal::moduleHandler()->moduleExists('views')) {
+        \Drupal::service('views.views_data')->clear();
+      }
+
+      // Run the post install tasks on the plugin.
+      $this->getConfigPlugin()->postInstall();
+
+      // We need to reset the plugin ID map cache as it will be out of date now.
+      $this->getConfigEnablerManager()->clearCachedPluginMaps();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageInterface $storage, array $entities) {
+    // When a GroupConfigType is deleted, we clear the views data cache to make
+    // sure that all of the views data which relies on group config types is up
+    // to date.
+    if (\Drupal::moduleHandler()->moduleExists('views')) {
+      \Drupal::service('views.views_data')->clear();
+    }
+
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager */
+    $plugin_manager = \Drupal::service('plugin.manager.group_config_enabler');
+
+    // We need to reset the plugin ID map cache as it will be out of date now.
+    $plugin_manager->clearCachedPluginMaps();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    // By adding the group type as a dependency, we ensure the group config
+    // type is deleted along with the group type.
+    $this->addDependency('config', $this->getGroupType()->getConfigDependencyName());
+
+    // Add the dependencies of the responsible config enabler plugin.
+    $this->addDependencies($this->getConfigPlugin()->calculateDependencies());
+  }
+
+}
diff --git a/src/Entity/GroupConfigTypeInterface.php b/src/Entity/GroupConfigTypeInterface.php
new file mode 100644
index 0000000..81ba214
--- /dev/null
+++ b/src/Entity/GroupConfigTypeInterface.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\group\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\EntityDescriptionInterface;
+
+/**
+ * Provides an interface defining a group config type entity.
+ */
+interface GroupConfigTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface {
+
+  /**
+   * Gets the group type the config type was created for.
+   *
+   * @return \Drupal\group\Entity\GroupTypeInterface
+   *   The group type for which the config type was created.
+   */
+  public function getGroupType();
+
+  /**
+   * Gets the group type ID the config type was created for.
+   *
+   * @return string
+   *   The group type ID for which the config type was created.
+   */
+  public function getGroupTypeId();
+
+  /**
+   * Gets the config enabler plugin the config type uses.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   *   The config enabler plugin the config type uses.
+   */
+  public function getConfigPlugin();
+
+  /**
+   * Gets the config enabler plugin ID the config type uses.
+   *
+   * @return string
+   *   The config enabler plugin ID the config type uses.
+   */
+  public function getConfigPluginId();
+
+  /**
+   * Updates the configuration of the config enabler plugin.
+   *
+   * Any keys that were left out will be reset to the default.
+   *
+   * @param array $configuration
+   *   An array of config enabler plugin configuration.
+   */
+  public function updateConfigPlugin(array $configuration);
+
+  /**
+   * Loads group config type entities by their responsible plugin ID.
+   *
+   * @param string|string[] $plugin_id
+   *   The ID of the config enabler plugin or an array of plugin IDs. If more
+   *   than one plugin ID is provided, this will load all of the group config
+   *   types that match any of the provided plugin IDs.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface[]
+   *   An array of group config type entities indexed by their IDs.
+   */
+  public static function loadByConfigPluginId($plugin_id);
+
+  /**
+   * Loads group config type entities which could serve a given entity type.
+   *
+   * @param string $entity_type_id
+   *   An entity type ID which may be served by one or more group config types.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface[]
+   *   An array of group config type entities which serve the given entity.
+   */
+  public static function loadByEntityTypeId($entity_type_id);
+
+}
diff --git a/src/Entity/GroupType.php b/src/Entity/GroupType.php
index c8ebd75..01066f4 100644
--- a/src/Entity/GroupType.php
+++ b/src/Entity/GroupType.php
@@ -42,6 +42,7 @@ use Drupal\Core\Entity\EntityStorageInterface;
  *     "add-form" = "/admin/group/types/add",
  *     "collection" = "/admin/group/types",
  *     "content-plugins" = "/admin/group/types/manage/{group_type}/content",
+ *     "config-plugins" = "/admin/group/types/manage/{group_type}/config",
  *     "delete-form" = "/admin/group/types/manage/{group_type}/delete",
  *     "edit-form" = "/admin/group/types/manage/{group_type}",
  *     "permissions-form" = "/admin/group/types/manage/{group_type}/permissions"
@@ -312,6 +313,16 @@ class GroupType extends ConfigEntityBundleBase implements GroupTypeInterface {
   }
 
   /**
+   * Returns the content enabler plugin manager.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   *   The group content plugin manager.
+   */
+  protected function getConfigEnablerManager() {
+    return \Drupal::service('plugin.manager.group_config_enabler');
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getInstalledContentPlugins() {
@@ -361,4 +372,54 @@ class GroupType extends ConfigEntityBundleBase implements GroupTypeInterface {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getInstalledConfigPlugins() {
+    return $this->getConfigEnablerManager()->getInstalled($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasConfigPlugin($plugin_id) {
+    $installed = $this->getConfigEnablerManager()->getInstalledIds($this);
+    return in_array($plugin_id, $installed);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigPlugin($plugin_id) {
+    return $this->getInstalledConfigPlugins()->get($plugin_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function installConfigPlugin($plugin_id, array $configuration = []) {
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage('group_config_type');
+    $storage->createFromPlugin($this, $plugin_id, $configuration)->save();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateConfigPlugin($plugin_id, array $configuration) {
+    $plugin = $this->getConfigPlugin($plugin_id);
+    GroupConfigType::load($plugin->getConfigTypeConfigId())->updateConfigPlugin($configuration);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstallConfigPlugin($plugin_id) {
+    $plugin = $this->getConfigPlugin($plugin_id);
+    GroupConfigType::load($plugin->getConfigTypeConfigId())->delete();
+    return $this;
+  }
+
 }
diff --git a/src/Entity/GroupTypeInterface.php b/src/Entity/GroupTypeInterface.php
index 0f2e378..0905112 100644
--- a/src/Entity/GroupTypeInterface.php
+++ b/src/Entity/GroupTypeInterface.php
@@ -194,4 +194,83 @@ interface GroupTypeInterface extends ConfigEntityInterface, EntityDescriptionInt
    */
   public function uninstallContentPlugin($plugin_id);
 
+  /**
+   * Returns the installed config enabler plugins for this group type.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerCollection
+   *   The group config plugin collection.
+   */
+  public function getInstalledConfigPlugins();
+
+  /**
+   * Checks whether a config enabler plugin is installed for this group type.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin to check for.
+   *
+   * @return bool
+   *   Whether the config enabler plugin is installed.
+   */
+  public function hasConfigPlugin($plugin_id);
+
+  /**
+   * Gets an installed config enabler plugin for this group type.
+   *
+   * Warning: In places where the plugin may not be installed on the group type,
+   * you should always run ::hasConfigPlugin() first or you may risk ending up
+   * with crashes or unreliable data.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   *   The installed config enabler plugin for the group type.
+   */
+  public function getConfigPlugin($plugin_id);
+
+  /**
+   * Adds a config enabler plugin to this group type.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin to add.
+   * @param array $configuration
+   *   (optional) An array of config enabler plugin configuration.
+   *
+   * @return $this
+   *
+   * @deprecated in Group 1.0-beta3, will be removed before Group 1.0-rc1. Use
+   *   \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface::
+   *   createFromPlugin() instead.
+   */
+  public function installConfigPlugin($plugin_id, array $configuration = []);
+
+  /**
+   * Updates the configuration of a config enabler plugin for this group type.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin to add.
+   * @param array $configuration
+   *   An array of config enabler plugin configuration.
+   *
+   * @return $this
+   *
+   * @deprecated in Group 1.0-beta3, will be removed before Group 1.0-rc1. Use
+   *   \Drupal\group\Entity\GroupConfigTypeInterface::updateConfigPlugin()
+   *   instead.
+   */
+  public function updateConfigPlugin($plugin_id, array $configuration);
+
+  /**
+   * Removes a config enabler plugin from this group type.
+   *
+   * @param string $plugin_id
+   *   The config enabler plugin ID.
+   *
+   * @return $this
+   *
+   * @deprecated in Group 1.0-beta3, will be removed before Group 1.0-rc1. Use
+   *   \Drupal\group\Entity\GroupConfigType::delete() instead.
+   */
+  public function uninstallConfigPlugin($plugin_id);
+
 }
diff --git a/src/Entity/Routing/GroupConfigRouteProvider.php b/src/Entity/Routing/GroupConfigRouteProvider.php
new file mode 100644
index 0000000..03b0f6b
--- /dev/null
+++ b/src/Entity/Routing/GroupConfigRouteProvider.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Drupal\group\Entity\Routing;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for group config.
+ */
+class GroupConfigRouteProvider extends DefaultHtmlRouteProvider {
+
+  /**
+   * The group config enabler plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a new GroupConfigRouteProvider.
+   *
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config enabler plugin manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   */
+  public function __construct(GroupConfigEnablerManagerInterface $plugin_manager, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
+    parent::__construct($entity_type_manager, $entity_field_manager);
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $container->get('plugin.manager.group_config_enabler'),
+      $container->get('entity_type.manager'),
+      $container->get('entity_field.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    if ($create_page_route = $this->getCreatePageRoute($entity_type)) {
+      $collection->add("entity.group_config.create_page", $create_page_route);
+    }
+
+    if ($create_form_route = $this->getCreateFormRoute($entity_type)) {
+      $collection->add("entity.group_config.create_form", $create_form_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAddPageRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getKey('bundle')) {
+      $route = new Route($entity_type->getLinkTemplate('add-page'));
+      $route
+        ->setDefault('_controller', '\Drupal\group\Entity\Controller\GroupConfigController::addPage')
+        ->setDefault('_title', 'Relate config to group')
+        ->setRequirement('_group_config_create_any_access', 'TRUE')
+        ->setOption('_group_operation_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAddFormRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('add-form')) {
+      $route = new Route($entity_type->getLinkTemplate('add-form'));
+      $route
+        ->setDefaults([
+          '_controller' => '\Drupal\group\Entity\Controller\GroupConfigController::addForm',
+          // @todo Let forms set title?
+          '_title_callback' => '\Drupal\group\Entity\Controller\GroupConfigController::addFormTitle',
+        ])
+        ->setRequirement('_group_config_create_access', 'TRUE')
+        ->setOption('_group_operation_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the create-page route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCreatePageRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('create-page') && $entity_type->getKey('bundle')) {
+      $route = new Route($entity_type->getLinkTemplate('create-page'));
+      $route
+        ->setDefault('_controller', '\Drupal\group\Entity\Controller\GroupConfigController::addPage')
+        ->setDefault('_title', 'Create config in group')
+        ->setDefault('create_mode', TRUE)
+        ->setRequirement('_group_config_create_any_entity_access', 'TRUE')
+        ->setOption('_group_operation_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the create-form route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCreateFormRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('create-form')) {
+      $route = new Route($entity_type->getLinkTemplate('create-form'));
+      $route
+        ->setDefaults([
+          '_controller' => '\Drupal\group\Entity\Controller\GroupConfigController::createForm',
+          // @todo Let forms set title?
+          '_title_callback' => '\Drupal\group\Entity\Controller\GroupConfigController::createFormTitle',
+        ])
+        ->setRequirement('_group_config_create_entity_access', 'TRUE')
+        ->setOption('_group_operation_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the collection route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass()) {
+      $route = new Route($entity_type->getLinkTemplate('collection'));
+      $route
+        ->addDefaults([
+          '_entity_list' => 'group_config',
+          '_title_callback' => '\Drupal\group\Entity\Controller\GroupConfigController::collectionTitle',
+        ])
+        ->setRequirement('_group_permission', "access config overview")
+        ->setOption('_group_operation_route', TRUE)
+        ->setOption('parameters', [
+          'group' => ['type' => 'entity:group'],
+        ]);
+
+      return $route;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
+    return parent::getCanonicalRoute($entity_type)
+      ->setRequirement('_group_owns_config', 'TRUE')
+      ->setOption('parameters', [
+        'group' => ['type' => 'entity:group'],
+        'group_config' => ['type' => 'entity:group_config'],
+      ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditFormRoute(EntityTypeInterface $entity_type) {
+    return parent::getEditFormRoute($entity_type)
+      ->setDefault('_title_callback', '\Drupal\group\Entity\Controller\GroupConfigController::editFormTitle')
+      ->setRequirement('_group_owns_config', 'TRUE')
+      ->setOption('_group_operation_route', TRUE)
+      ->setOption('parameters', [
+        'group' => ['type' => 'entity:group'],
+        'group_config' => ['type' => 'entity:group_config'],
+      ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
+    return parent::getDeleteFormRoute($entity_type)
+      ->setRequirement('_group_owns_config', 'TRUE')
+      ->setOption('_group_operation_route', TRUE)
+      ->setOption('parameters', [
+        'group' => ['type' => 'entity:group'],
+        'group_config' => ['type' => 'entity:group_config'],
+      ]);
+  }
+
+}
diff --git a/src/Entity/Storage/GroupConfigStorage.php b/src/Entity/Storage/GroupConfigStorage.php
new file mode 100644
index 0000000..5a433c7
--- /dev/null
+++ b/src/Entity/Storage/GroupConfigStorage.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\group\Entity\Storage;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\group\Entity\GroupInterface;
+
+/**
+ * Defines the storage handler class for group config entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * loading group config entities based on group and plugin information.
+ */
+class GroupConfigStorage extends SqlContentEntityStorage implements GroupConfigStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createForEntityInGroup(EntityInterface $entity, GroupInterface $group, $plugin_id, $values = []) {
+    // An unsaved entity cannot have any group config.
+    if ($entity->id() === NULL) {
+      throw new EntityStorageException("Cannot add an unsaved entity to a group.");
+    }
+
+    // An unsaved group cannot have any config.
+    if ($group->id() === NULL) {
+      throw new EntityStorageException("Cannot add an entity to an unsaved group.");
+    }
+
+    // Check whether the entity can actually be added to the group.
+    $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+    if ($entity->getEntityTypeId() != $plugin->getEntityTypeId()) {
+      throw new EntityStorageException("Invalid plugin provided for adding the entity to the group.");
+    }
+
+    // Verify the bundle as well if the plugin is specific about them.
+    $supported_bundle = $plugin->getEntityBundle();
+    if ($supported_bundle !== FALSE) {
+      if ($entity->bundle() != $supported_bundle) {
+        throw new EntityStorageException("The provided plugin provided does not support the entity's bundle.");
+      }
+    }
+
+    // Set the necessary keys for a valid GroupConfig entity.
+    $keys = [
+      'type' => $plugin->getConfigTypeConfigId(),
+      'gid' => $group->id(),
+      'entity_id' => $entity->id(),
+    ];
+
+    // Return an unsaved GroupConfig entity.
+    return $this->create($keys + $values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByGroup(GroupInterface $group, $plugin_id = NULL, $filters = []) {
+    // An unsaved group cannot have any config.
+    if ($group->id() === NULL) {
+      throw new EntityStorageException("Cannot load GroupConfig entities for an unsaved group.");
+    }
+
+    $properties = ['gid' => $group->id()] + $filters;
+
+    // If a plugin ID was provided, set the group config type ID for it.
+    if (isset($plugin_id)) {
+      /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+      $plugin = $group->getGroupType()->getConfigPlugin($plugin_id);
+      $properties['type'] = $plugin->getConfigTypeConfigId();
+    }
+
+    return $this->loadByProperties($properties);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByEntity(EntityInterface $entity) {
+    // An unsaved entity cannot have any group config.
+    if ($entity->id() === NULL) {
+      throw new EntityStorageException("Cannot load GroupConfig entities for an unsaved entity.");
+    }
+
+    // If no responsible group config types were found, we return nothing.
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('group_config_type');
+    $group_config_types = $storage->loadByEntityTypeId($entity->getEntityTypeId());
+    if (empty($group_config_types)) {
+      return [];
+    }
+
+    return $this->loadByProperties([
+      'type' => array_keys($group_config_types),
+      'entity_id' => $entity->id(),
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByConfigPluginId($plugin_id) {
+    // If no responsible group config types were found, we return nothing.
+    /** @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('group_config_type');
+    $group_config_types = $storage->loadByConfigPluginId($plugin_id);
+    if (empty($group_config_types)) {
+      return [];
+    }
+
+    return $this->loadByProperties(['type' => array_keys($group_config_types)]);
+  }
+
+}
diff --git a/src/Entity/Storage/GroupConfigStorageInterface.php b/src/Entity/Storage/GroupConfigStorageInterface.php
new file mode 100644
index 0000000..667e134
--- /dev/null
+++ b/src/Entity/Storage/GroupConfigStorageInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\group\Entity\Storage;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\group\Entity\GroupInterface;
+
+/**
+ * Defines an interface for group config entity storage classes.
+ */
+interface GroupConfigStorageInterface extends ContentEntityStorageInterface {
+
+  /**
+   * Creates a GroupConfig entity for placing a config entity in a group.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The config entity to add to the group.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to add the config entity to.
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin to add the entity with.
+   * @param array $values
+   *   (optional) Extra values to add to the GroupConfig entity.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface
+   *   A new GroupConfig entity.
+   */
+  public function createForEntityInGroup(EntityInterface $entity, GroupInterface $group, $plugin_id, $values = []);
+
+  /**
+   * Retrieves all GroupConfig entities for a group.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group entity to load the group config entities for.
+   * @param string $plugin_id
+   *   (optional) A config enabler plugin ID to filter on.
+   * @param array $filters
+   *   (optional) An associative array of extra filters where the keys are
+   *   property or field names and the values are the value to filter on.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface[]
+   *   A list of GroupConfig entities matching the criteria.
+   */
+  public function loadByGroup(GroupInterface $group, $plugin_id = NULL, $filters = []);
+
+  /**
+   * Retrieves all GroupConfig entities that represent a given entity.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   An entity which may be within one or more groups.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface[]
+   *   A list of GroupConfig entities which refer to the given entity.
+   */
+  public function loadByEntity(EntityInterface $entity);
+
+  /**
+   * Retrieves all GroupConfig entities by their responsible plugin ID.
+   *
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin.
+   *
+   * @return \Drupal\group\Entity\GroupConfigInterface[]
+   *   A list of GroupConfig entities indexed by their IDs.
+   */
+  public function loadByConfigPluginId($plugin_id);
+
+}
diff --git a/src/Entity/Storage/GroupConfigTypeStorage.php b/src/Entity/Storage/GroupConfigTypeStorage.php
new file mode 100644
index 0000000..883690a
--- /dev/null
+++ b/src/Entity/Storage/GroupConfigTypeStorage.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\group\Entity\Storage;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\group\Entity\GroupTypeInterface;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the storage handler class for group config type entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * loading group config type entities based on group type and plugin ID.
+ */
+class GroupConfigTypeStorage extends ConfigEntityStorage implements GroupConfigTypeStorageInterface {
+
+  /**
+   * The group config plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a GroupConfigTypeStorage object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config enabler manager.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
+   *   The UUID service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, GroupConfigEnablerManagerInterface $plugin_manager, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
+    parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('plugin.manager.group_config_enabler'),
+      $container->get('config.factory'),
+      $container->get('uuid'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByGroupType(GroupTypeInterface $group_type) {
+    return $this->loadByProperties(['group_type' => $group_type->id()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByConfigPluginId($plugin_id) {
+    return $this->loadByProperties(['config_plugin' => $plugin_id]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByEntityTypeId($entity_type_id) {
+    $plugin_ids = [];
+
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) {
+      if ($plugin->getEntityTypeId() === $entity_type_id) {
+        $plugin_ids[] = $plugin_id;
+      }
+    }
+
+    // If no responsible group config plugins were found, we return nothing.
+    if (empty($plugin_ids)) {
+      return [];
+    }
+
+    // Otherwise load all group config types being handled by gathered plugins.
+    return $this->loadByConfigPluginId($plugin_ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFromPlugin(GroupTypeInterface $group_type, $plugin_id, array $configuration = []) {
+    // Add the group type ID to the configuration.
+    $configuration['group_type_id'] = $group_type->id();
+
+    // Instantiate the plugin we are installing.
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    $plugin = $this->pluginManager->createInstance($plugin_id, $configuration);
+
+    // Create the group config type using plugin generated info.
+    $values = [
+      'id' => $plugin->getConfigTypeConfigId(),
+      'label' => $plugin->getConfigTypeLabel(),
+      'description' => $plugin->getConfigTypeDescription(),
+      'group_type' => $group_type->id(),
+      'config_plugin' => $plugin_id,
+      'plugin_config' => $plugin->getConfiguration(),
+    ];
+
+    return $this->create($values);
+  }
+
+}
diff --git a/src/Entity/Storage/GroupConfigTypeStorageInterface.php b/src/Entity/Storage/GroupConfigTypeStorageInterface.php
new file mode 100644
index 0000000..3ce0697
--- /dev/null
+++ b/src/Entity/Storage/GroupConfigTypeStorageInterface.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\group\Entity\Storage;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
+use Drupal\group\Entity\GroupTypeInterface;
+
+/**
+ * Defines an interface for group config type entity storage classes.
+ */
+interface GroupConfigTypeStorageInterface extends ConfigEntityStorageInterface {
+
+  /**
+   * Retrieves all group config types for a group type.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   The group type to load the group config types for.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface[]
+   *   An array of group config types indexed by their IDs.
+   */
+  public function loadByGroupType(GroupTypeInterface $group_type);
+
+  /**
+   * Retrieves group config types by their responsible plugin ID.
+   *
+   * @param string|string[] $plugin_id
+   *   The ID of the config enabler plugin or an array of plugin IDs. If more
+   *   than one plugin ID is provided, this will load all of the group config
+   *   types that match any of the provided plugin IDs.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface[]
+   *   An array of group config types indexed by their IDs.
+   */
+  public function loadByConfigPluginId($plugin_id);
+
+  /**
+   * Retrieves group config types which could serve a given entity type.
+   *
+   * @param string $entity_type_id
+   *   An entity type ID which may be served by one or more group config types.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface[]
+   *   An array of group config types indexed by their IDs.
+   */
+  public function loadByEntityTypeId($entity_type_id);
+
+  /**
+   * Creates a group config type for a group type using a specific plugin.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   The group type to create the group config type for.
+   * @param string $plugin_id
+   *   The ID of the config enabler plugin to use.
+   * @param array $configuration
+   *   (optional) An array of config enabler plugin configuration.
+   *
+   * @return \Drupal\group\Entity\GroupConfigTypeInterface
+   *   A new, unsaved GroupConfigType entity.
+   */
+  public function createFromPlugin(GroupTypeInterface $group_type, $plugin_id, array $configuration = []);
+
+}
diff --git a/src/Entity/Views/GroupConfigViewsData.php b/src/Entity/Views/GroupConfigViewsData.php
new file mode 100644
index 0000000..212804c
--- /dev/null
+++ b/src/Entity/Views/GroupConfigViewsData.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\group\Entity\Views;
+
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Drupal\views\EntityViewsData;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the views data for the group config entity type.
+ */
+class GroupConfigViewsData extends EntityViewsData {
+
+  /**
+   * The group config enabler plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * The entity manager set but not declared in the parent class.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a GroupConfigViewsData object.
+   *
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config enabler plugin manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, SqlEntityStorageInterface $storage_controller, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager, GroupConfigEnablerManagerInterface $plugin_manager) {
+    parent::__construct($entity_type, $storage_controller, $entity_manager, $module_handler, $translation_manager);
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('entity.manager'),
+      $container->get('module_handler'),
+      $container->get('string_translation'),
+      $container->get('plugin.manager.group_config_enabler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Add a custom numeric argument for the parent group ID that allows us to
+    // use replacement titles with the parent group's label.
+    $data['group_config_field_data']['gid']['argument'] = [
+      'id' => 'group_id',
+      'numeric' => TRUE,
+    ];
+
+    // Get the data table for GroupConfig entities.
+    $data_table = $this->entityType->getDataTable();
+
+    // Unset the 'entity_id' field relationship as we want a more powerful one.
+    // @todo Eventually, we may want to replace all of 'entity_id'.
+    unset($data[$data_table]['entity_id']['relationship']);
+
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types = $this->entityManager->getDefinitions();
+
+    // Add views data for all defined plugins so modules can provide default
+    // views even though their plugins may not have been installed yet.
+    foreach ($this->pluginManager->getAll() as $plugin) {
+      $entity_type_id = $plugin->getEntityTypeId();
+      $entity_type = $entity_types[$entity_type_id];
+      $entity_data_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
+
+      // Create a unique field name for this views field.
+      $field_name = 'gc__' . $entity_type_id;
+
+      // We only add one 'group_config' relationship per entity type.
+      if (isset($data[$entity_data_table][$field_name])) {
+        continue;
+      }
+
+      $t_args = [
+        '@entity_type' => $entity_type->getLabel(),
+      ];
+
+      // This relationship will allow a group config entity to easily map to a
+      // config entity that it ties to a group, optionally filtering by plugin.
+      $data[$data_table][$field_name] = [
+        'title' => $this->t('@entity_type from group config', $t_args),
+        'help' => $this->t('Relates to the @entity_type entity the group config represents.', $t_args),
+        'relationship' => [
+          'group' => $entity_type->getLabel(),
+          'base' => $entity_data_table,
+          'base field' => $entity_type->getKey('id'),
+          'relationship field' => 'entity_id',
+          'id' => 'group_config_to_entity',
+          'label' => $this->t('Group config @entity_type', $t_args),
+          'target_entity_type' => $entity_type_id,
+        ],
+      ];
+    }
+
+    return $data;
+  }
+
+}
diff --git a/src/Form/GroupConfigJoinForm.php b/src/Form/GroupConfigJoinForm.php
new file mode 100644
index 0000000..12fc0df
--- /dev/null
+++ b/src/Form/GroupConfigJoinForm.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\group\Form;
+
+use Drupal\group\Entity\Form\GroupConfigForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a form for joining a group.
+ */
+class GroupConfigJoinForm extends GroupConfigForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['entity_id']['#access'] = FALSE;
+    $form['group_roles']['#access'] = FALSE;
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+    $actions['submit']['#value'] = $this->t('Join group');
+    return $actions;
+  }
+
+}
diff --git a/src/Form/GroupConfigLeaveForm.php b/src/Form/GroupConfigLeaveForm.php
new file mode 100644
index 0000000..13a99a0
--- /dev/null
+++ b/src/Form/GroupConfigLeaveForm.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\group\Form;
+
+use Drupal\group\Entity\Form\GroupConfigDeleteForm;
+
+/**
+ * Provides a form for leaving a group.
+ */
+class GroupConfigLeaveForm extends GroupConfigDeleteForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    $message = 'Are you sure you want to leave %group?';
+    $replace = ['%group' => $this->getEntity()->getGroup()->label()];
+    return $this->t($message, $replace);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Leave group');
+  }
+
+}
diff --git a/src/Plugin/GroupConfigEnablerBase.php b/src/Plugin/GroupConfigEnablerBase.php
new file mode 100644
index 0000000..5cb7392
--- /dev/null
+++ b/src/Plugin/GroupConfigEnablerBase.php
@@ -0,0 +1,562 @@
+<?php
+
+namespace Drupal\group\Plugin;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\group\Access\GroupAccessResult;
+use Drupal\group\Entity\GroupType;
+use Drupal\group\Entity\GroupInterface;
+use Drupal\group\Entity\GroupConfigInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides a base class for GroupConfigEnabler plugins.
+ *
+ * @see \Drupal\group\Annotation\GroupConfigEnabler
+ * @see \Drupal\group\GroupConfigEnablerManager
+ * @see \Drupal\group\Plugin\GroupConfigEnablerInterface
+ * @see plugin_api
+ */
+abstract class GroupConfigEnablerBase extends PluginBase implements GroupConfigEnablerInterface {
+
+  /**
+   * The ID of group type this plugin was instantiated for.
+   *
+   * @var string
+   */
+  protected $groupTypeId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    // Only support setting the group type ID during construction.
+    if (!empty($configuration['group_type_id'])) {
+      $this->groupTypeId = $configuration['group_type_id'];
+    }
+
+    // Include the default configuration by calling ::setConfiguration().
+    $this->setConfiguration($configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider() {
+    return $this->pluginDefinition['provider'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->pluginDefinition['description'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return $this->pluginDefinition['entity_type_id'];
+  }
+
+  /**
+   * Returns the entity type definition the plugin supports.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   *   The entity type definition.
+   */
+  protected function getEntityType() {
+    return \Drupal::entityTypeManager()->getDefinition($this->getEntityTypeId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityBundle() {
+    return $this->pluginDefinition['entity_bundle'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPrettyPathKey() {
+    return $this->pluginDefinition['pretty_path_key'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupCardinality() {
+    return $this->configuration['group_cardinality'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityCardinality() {
+    return $this->configuration['entity_cardinality'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupType() {
+    if ($id = $this->getGroupTypeId()) {
+      return GroupType::load($id);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupTypeId() {
+    return $this->groupTypeId;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function definesEntityAccess() {
+    return $this->pluginDefinition['entity_access'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEnforced() {
+    return $this->pluginDefinition['enforced'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigLabel(GroupConfigInterface $group_config) {
+    return $group_config->getEntity()->label();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigTypeConfigId() {
+    $preferred_id = $this->getGroupTypeId() . '-' . str_replace(':', '-', $this->getPluginId());
+
+    // Return a hashed ID if the readable ID would exceed the maximum length.
+    if (strlen($preferred_id) > EntityTypeInterface::BUNDLE_MAX_LENGTH) {
+      $hashed_id = 'group_config_type_' . md5($preferred_id);
+      $preferred_id = substr($hashed_id, 0, EntityTypeInterface::BUNDLE_MAX_LENGTH);
+    }
+
+    return $preferred_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigTypeLabel() {
+    return $this->getGroupType()->label() . ': ' . $this->getLabel();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigTypeDescription() {
+    return $this->getDescription();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupOperations(GroupInterface $group) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations() {
+    return [];
+  }
+
+  /**
+   * Provides permissions for the group config entity; i.e. the relationship.
+   *
+   * @return array
+   *   An array of group permissions, see ::getPermissions for more info.
+   *
+   * @see GroupConfigEnablerInterface::getPermissions()
+   */
+  protected function getGroupConfigPermissions() {
+    $plugin_id = $this->getPluginId();
+
+    // Allow permissions here and in child classes to easily use the plugin name
+    // and target entity type name in their titles and descriptions.
+    $t_args = [
+      '%plugin_name' => $this->getLabel(),
+      '%entity_type' => $this->getEntityType()->getLowercaseLabel(),
+    ];
+    $defaults = ['title_args' => $t_args, 'description_args' => $t_args];
+
+    // Use the same title prefix to keep permissions sorted properly.
+    $prefix = '%plugin_name - Relationship:';
+
+    $permissions["view $plugin_id config"] = [
+      'title' => "$prefix View entity relations",
+    ] + $defaults;
+
+    $permissions["create $plugin_id config"] = [
+      'title' => "$prefix Add entity relation",
+      'description' => 'Allows you to relate an existing %entity_type entity to the group.',
+    ] + $defaults;
+
+    $permissions["update own $plugin_id config"] = [
+      'title' => "$prefix Edit own entity relations",
+    ] + $defaults;
+
+    $permissions["update any $plugin_id config"] = [
+      'title' => "$prefix Edit any entity relation",
+    ] + $defaults;
+
+    $permissions["delete own $plugin_id config"] = [
+      'title' => "$prefix Delete own entity relations",
+    ] + $defaults;
+
+    $permissions["delete any $plugin_id config"] = [
+      'title' => "$prefix Delete any entity relation",
+    ] + $defaults;
+
+    return $permissions;
+  }
+
+  /**
+   * Provides permissions for the actual entity being added to the group.
+   *
+   * @return array
+   *   An array of group permissions, see ::getPermissions for more info.
+   *
+   * @see GroupConfigEnablerInterface::getPermissions()
+   */
+  protected function getTargetEntityPermissions() {
+    $plugin_id = $this->getPluginId();
+
+    // Allow permissions here and in child classes to easily use the plugin and
+    // target entity type labels in their titles and descriptions.
+    $t_args = [
+      '%plugin_name' => $this->getLabel(),
+      '%entity_type' => $this->getEntityType()->getLowercaseLabel(),
+    ];
+    $defaults = ['title_args' => $t_args, 'description_args' => $t_args];
+
+    // Use the same title prefix to keep permissions sorted properly.
+    $prefix = '%plugin_name - Entity:';
+
+    $permissions["view $plugin_id entity"] = [
+      'title' => "$prefix View %entity_type entities",
+    ] + $defaults;
+
+    $permissions["create $plugin_id entity"] = [
+      'title' => "$prefix Add %entity_type entities",
+      'description' => 'Allows you to create a new %entity_type entity and relate it to the group.',
+    ] + $defaults;
+
+    $permissions["update own $plugin_id entity"] = [
+      'title' => "$prefix Edit own %entity_type entities",
+    ] + $defaults;
+
+    $permissions["update any $plugin_id entity"] = [
+      'title' => "$prefix Edit any %entity_type entities",
+    ] + $defaults;
+
+    $permissions["delete own $plugin_id entity"] = [
+      'title' => "$prefix Delete own %entity_type entities",
+    ] + $defaults;
+
+    $permissions["delete any $plugin_id entity"] = [
+      'title' => "$prefix Delete any %entity_type entities",
+    ] + $defaults;
+
+    return $permissions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPermissions() {
+    $permissions = $this->getGroupConfigPermissions();
+    if ($this->definesEntityAccess()) {
+      $permissions += $this->getTargetEntityPermissions();
+    }
+    return $permissions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createEntityAccess(GroupInterface $group, AccountInterface $account) {
+    // You cannot create target entities if the plugin does not support it.
+    if (!$this->definesEntityAccess()) {
+      return AccessResult::neutral();
+    }
+
+    $plugin_id = $this->getPluginId();
+    return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "create $plugin_id entity");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createAccess(GroupInterface $group, AccountInterface $account) {
+    $plugin_id = $this->getPluginId();
+    return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "create $plugin_id config");
+  }
+
+  /**
+   * Performs access check for the view operation.
+   *
+   * This method is supposed to be overwritten by extending classes that
+   * do their own custom access checking.
+   *
+   * @param \Drupal\group\Entity\GroupConfigInterface $group_config
+   *   The group config for which to check access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  protected function viewAccess(GroupConfigInterface $group_config, AccountInterface $account) {
+    $group = $group_config->getGroup();
+    $plugin_id = $this->getPluginId();
+    return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "view $plugin_id config");
+  }
+
+  /**
+   * Performs access check for the update operation.
+   *
+   * This method is supposed to be overwritten by extending classes that
+   * do their own custom access checking.
+   *
+   * @param \Drupal\group\Entity\GroupConfigInterface $group_config
+   *   The group config for which to check access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  protected function updateAccess(GroupConfigInterface $group_config, AccountInterface $account) {
+    $group = $group_config->getGroup();
+    $plugin_id = $this->getPluginId();
+
+    // Allow members to edit their own group config.
+    if ($group_config->getOwnerId() == $account->id()) {
+      return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "update own $plugin_id config");
+    }
+
+    return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "update any $plugin_id config");
+  }
+
+  /**
+   * Performs access check for the delete operation.
+   *
+   * This method is supposed to be overwritten by extending classes that
+   * do their own custom access checking.
+   *
+   * @param \Drupal\group\Entity\GroupConfigInterface $group_config
+   *   The group config for which to check access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  protected function deleteAccess(GroupConfigInterface $group_config, AccountInterface $account) {
+    $group = $group_config->getGroup();
+    $plugin_id = $this->getPluginId();
+
+    // Allow members to delete their own group config.
+    if ($group_config->getOwnerId() == $account->id()) {
+      return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "delete own $plugin_id config");
+    }
+
+    return GroupAccessResult::allowedIfHasGroupPermission($group, $account, "delete any $plugin_id config");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccess(GroupConfigInterface $group_config, $operation, AccountInterface $account) {
+    switch ($operation) {
+      case 'view':
+        $result = $this->viewAccess($group_config, $account);
+        break;
+
+      case 'update':
+        $result = $this->updateAccess($group_config, $account);
+        break;
+
+      case 'delete':
+        $result = $this->deleteAccess($group_config, $account);
+        break;
+
+      default:
+        $result = GroupAccessResult::neutral();
+    }
+
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityReferenceLabel() {
+    return isset($this->pluginDefinition['reference_label'])
+      ? $this->pluginDefinition['reference_label']
+      : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityReferenceDescription() {
+    return isset($this->pluginDefinition['reference_description'])
+      ? $this->pluginDefinition['reference_description']
+      : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityReferenceSettings() {
+    $settings['target_type'] = $this->getEntityTypeId();
+    if ($bundle = $this->getEntityBundle()) {
+      $settings['handler_settings']['target_bundles'] = [$bundle];
+    }
+    return $settings;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postInstall() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    // Do not allow the changing of the group type ID after construction.
+    unset($configuration['group_type_id']);
+
+    // Merge in the default configuration.
+    $this->configuration = NestedArray::mergeDeep(
+      $this->defaultConfiguration(),
+      $configuration
+    );
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    // Warning: For every key defined here you need to have a matching config
+    // schema entry following the pattern group_config_enabler.config.MY_KEY!
+    // @see group.schema.yml
+    return [
+      'group_cardinality' => 0,
+      'entity_cardinality' => 0,
+      'use_creation_wizard' => 1,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
+    $entity_type_manager = \Drupal::service('entity_type.manager');
+
+    $replace = [
+      '%entity_type' => $entity_type_manager->getDefinition($this->getEntityTypeId())->getLabel(),
+      '%group_type' => $this->getGroupType()->label(),
+      '%plugin' => $this->getLabel(),
+    ];
+
+    $form['group_cardinality'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Group cardinality'),
+      '#description' => $this->t('The amount of %group_type groups a single %entity_type entity can be added to as a %plugin. Set to 0 for unlimited.', $replace),
+      '#default_value' => $this->configuration['group_cardinality'],
+      '#min' => 0,
+      '#required' => TRUE,
+    ];
+
+    $form['entity_cardinality'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Entity cardinality'),
+      '#description' => $this->t('The amount of times a single %entity_type entity can be added to the same %group_type group as a %plugin. Set to 0 for unlimited.', $replace),
+      '#default_value' => $this->configuration['entity_cardinality'],
+      '#min' => 0,
+      '#required' => TRUE,
+    ];
+
+    if ($this->definesEntityAccess()) {
+      $form['use_creation_wizard'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Use 2-step wizard when creating a new %entity_type entity within a %group_type group', $replace),
+        '#description' => $this->t('This will first show you the form to create the actual entity and then a form to create the relationship between the entity and the group.<br />You can choose to disable this wizard if you did not or will not add any fields to the relationship (i.e. this plugin).<br /><strong>Warning:</strong> If you do have fields on the relationship and do not use the wizard, you may end up with required fields not being filled out.'),
+        '#default_value' => $this->configuration['use_creation_wizard'],
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Only override this function if you need to do something specific to the
+   * submitted data before it is saved as configuration on the plugin. The data
+   * gets saved on the plugin in \Drupal\group\Entity\Form\GroupConfigTypeForm.
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    $dependencies['module'][] = $this->getProvider();
+    $dependencies['module'][] = $this->getEntityType()->getProvider();
+    return $dependencies;
+  }
+
+}
diff --git a/src/Plugin/GroupConfigEnablerCollection.php b/src/Plugin/GroupConfigEnablerCollection.php
new file mode 100644
index 0000000..423c056
--- /dev/null
+++ b/src/Plugin/GroupConfigEnablerCollection.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\group\Plugin;
+
+use Drupal\Core\Plugin\DefaultLazyPluginCollection;
+
+/**
+ * A collection of group config plugins.
+ */
+class GroupConfigEnablerCollection extends DefaultLazyPluginCollection {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerInterface
+   */
+  public function &get($instance_id) {
+    return parent::get($instance_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Sorts plugins by provider.
+   */
+  public function sortHelper($aID, $bID) {
+    $a = $this->get($aID);
+    $b = $this->get($bID);
+
+    if ($a->getProvider() != $b->getProvider()) {
+      return strnatcasecmp($a->getProvider(), $b->getProvider());
+    }
+
+    return parent::sortHelper($aID, $bID);
+  }
+
+}
diff --git a/src/Plugin/GroupConfigEnablerInterface.php b/src/Plugin/GroupConfigEnablerInterface.php
new file mode 100644
index 0000000..26ec0b7
--- /dev/null
+++ b/src/Plugin/GroupConfigEnablerInterface.php
@@ -0,0 +1,305 @@
+<?php
+
+namespace Drupal\group\Plugin;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\group\Entity\GroupConfigInterface;
+use Drupal\group\Entity\GroupInterface;
+
+/**
+ * Defines an interface for pluggable GroupConfigEnabler back-ends.
+ *
+ * @see \Drupal\group\Annotation\GroupConfigEnabler
+ * @see \Drupal\group\GroupConfigEnablerManager
+ * @see \Drupal\group\Plugin\GroupConfigEnablerBase
+ * @see plugin_api
+ */
+interface GroupConfigEnablerInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface, PluginFormInterface {
+
+  /**
+   * Returns the plugin provider.
+   *
+   * @return string
+   */
+  public function getProvider();
+
+  /**
+   * Returns the administrative label for the plugin.
+   *
+   * @return string
+   */
+  public function getLabel();
+
+  /**
+   * Returns the administrative description for the plugin.
+   *
+   * @return string
+   */
+  public function getDescription();
+
+  /**
+   * Returns the entity type ID the plugin supports.
+   *
+   * @return string
+   *   The entity type ID.
+   */
+  public function getEntityTypeId();
+
+  /**
+   * Returns the entity bundle the plugin supports.
+   *
+   * @return string|false
+   *   The bundle name or FALSE in case it supports all bundles.
+   */
+  public function getEntityBundle();
+
+  /**
+   * Returns the pretty path key for use in path aliases.
+   *
+   * @return string
+   *   The plugin-provided pretty path key, defaults to 'config'.
+   */
+  public function getPrettyPathKey();
+
+  /**
+   * Returns the amount of groups the same config can be added to.
+   *
+   * @return int
+   *   The group config's group cardinality.
+   */
+  public function getGroupCardinality();
+
+  /**
+   * Returns the amount of times the same config can be added to a group.
+   *
+   * @return int
+   *   The group config's entity cardinality.
+   */
+  public function getEntityCardinality();
+
+  /**
+   * Returns the group type the plugin was instantiated for.
+   *
+   * @return \Drupal\group\Entity\GroupTypeInterface|null
+   *   The group type, if set in the plugin configuration.
+   */
+  public function getGroupType();
+
+  /**
+   * Returns the ID of the group type the plugin was instantiated for.
+   *
+   * @return string|null
+   *   The group type ID, if set in the plugin configuration.
+   */
+  public function getGroupTypeId();
+
+  /**
+   * Returns whether this plugin defines entity access.
+   *
+   * @return bool
+   *   Whether this plugin defines entity access.
+   *
+   * @see \Drupal\group\Annotation\GroupConfigEnabler::$entity_access
+   */
+  public function definesEntityAccess();
+
+  /**
+   * Returns whether this plugin is always on.
+   *
+   * @return bool
+   *   The 'enforced' status.
+   */
+  public function isEnforced();
+
+  /**
+   * Retrieves the label for a piece of group config.
+   *
+   * @param \Drupal\group\Entity\GroupConfigInterface $group_config
+   *
+   * @return string
+   *   The label as expected by \Drupal\Core\Entity\EntityInterface::label().
+   */
+  public function getConfigLabel(GroupConfigInterface $group_config);
+
+  /**
+   * Returns a safe, unique configuration ID for a group config type.
+   *
+   * By default we use GROUP_TYPE_ID-PLUGIN_ID-DERIVATIVE_ID, but feel free to
+   * use any other means of identifying group config types.
+   *
+   * Please do not return any invalid characters in the ID as it will crash the
+   * website. Refer to ConfigBase::validateName() for valid characters.
+   *
+   * @return string
+   *   The safe ID to use as the configuration name.
+   *
+   * @see \Drupal\Core\Config\ConfigBase::validateName()
+   */
+  public function getConfigTypeConfigId();
+
+  /**
+   * Returns the administrative label for a group config type.
+   *
+   * @return string
+   */
+  public function getConfigTypeLabel();
+
+  /**
+   * Returns the administrative description for a group config type.
+   *
+   * @return string
+   */
+  public function getConfigTypeDescription();
+
+  /**
+   * Provides a list of operations for a group.
+   *
+   * These operations can be implemented in numerous ways by extending modules.
+   * Out of the box, Group provides a block that shows the available operations
+   * to a user visiting a route with a group in its URL.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to generate the operations for.
+   *
+   * @return array
+   *   An associative array of operation links to show when in a group context,
+   *   keyed by operation name, containing the following key-value pairs:
+   *   - title: The localized title of the operation.
+   *   - url: An instance of \Drupal\Core\Url for the operation URL.
+   *   - weight: The weight of the operation.
+   */
+  public function getGroupOperations(GroupInterface $group);
+
+  /**
+   * Provides a list of operations for the config enabler plugin.
+   *
+   * These operations will be merged with the ones already available on the
+   * group type config configuration page: (un)install, manage fields, etc.
+   *
+   * @return array
+   *   An associative array of operation links to show on the group type config
+   *   administration UI, keyed by operation name, containing the following
+   *   key-value pairs:
+   *   - title: The localized title of the operation.
+   *   - url: An instance of \Drupal\Core\Url for the operation URL.
+   *   - weight: The weight of this operation.
+   */
+  public function getOperations();
+
+  /**
+   * Provides a list of group permissions the plugin exposes.
+   *
+   * If you have some group permissions that would only make sense when your
+   * plugin is installed, you may define those here. They will not be shown on
+   * the permission configuration form unless the plugin is installed.
+   *
+   * @return array
+   *   An array of group permissions, see GroupPermissionHandlerInterface for
+   *   the structure of a group permission.
+   *
+   * @see GroupPermissionHandlerInterface::getPermissions()
+   */
+  public function getPermissions();
+
+  /**
+   * Performs access check for the create target entity operation.
+   *
+   * This method is supposed to be overwritten by extending classes that
+   * do their own custom access checking.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to check for target entity creation access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function createEntityAccess(GroupInterface $group, AccountInterface $account);
+
+  /**
+   * Performs access check for the create operation.
+   *
+   * This method is supposed to be overwritten by extending classes that
+   * do their own custom access checking.
+   *
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group to check for config creation access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function createAccess(GroupInterface $group, AccountInterface $account);
+
+  /**
+   * Checks access to an operation on a given group config entity.
+   *
+   * Use \Drupal\group\Plugin\GroupConfigEnablerInterface::createAccess() to
+   * check access to create a group config entity.
+   *
+   * @param \Drupal\group\Entity\GroupConfigInterface $group_config
+   *   The group config for which to check access.
+   * @param string $operation
+   *   The operation access should be checked for. Usually one of "view",
+   *   "update" or "delete".
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user session for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function checkAccess(GroupConfigInterface $group_config, $operation, AccountInterface $account);
+
+  /**
+   * Returns the label for the entity reference field.
+   *
+   * This allows you to specify the label for the entity reference field
+   * pointing to the entity that is to become group config.
+   *
+   * @return string|null
+   *   The label for the entity reference field or NULL if none was set.
+   */
+  public function getEntityReferenceLabel();
+
+  /**
+   * Returns the description for the entity reference field.
+   *
+   * This allows you to specify the description for the entity reference field
+   * pointing to the entity that is to become group config.
+   *
+   * @return string|null
+   *   The description for the entity reference field or NULL if none was set.
+   */
+  public function getEntityReferenceDescription();
+
+  /**
+   * Returns a list of entity reference field settings.
+   *
+   * This allows you to provide some handler settings for the entity reference
+   * field pointing to the entity that is to become group config. You could
+   * even change the handler being used, all without having to alter the bundle
+   * field settings yourself through an alter hook.
+   *
+   * @return array
+   *   An associative array where the keys are valid entity reference field
+   *   setting names and the values are the corresponding setting for each key.
+   *   Often used keys are 'target_type', 'handler' and 'handler_settings'.
+   */
+  public function getEntityReferenceSettings();
+
+  /**
+   * Runs tasks after the group config type for this plugin has been created.
+   *
+   * A good example of what you might want to do here, is the installation of
+   * extra locked fields on the group config type. You can find an example in
+   * \Drupal\group\Plugin\GroupConfigEnabler\GroupMembership::postInstall().
+   */
+  public function postInstall();
+
+}
diff --git a/src/Plugin/GroupConfigEnablerManager.php b/src/Plugin/GroupConfigEnablerManager.php
new file mode 100644
index 0000000..eb861b9
--- /dev/null
+++ b/src/Plugin/GroupConfigEnablerManager.php
@@ -0,0 +1,422 @@
+<?php
+
+namespace Drupal\group\Plugin;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\group\Entity\GroupTypeInterface;
+
+/**
+ * Manages GroupConfigEnabler plugin implementations.
+ *
+ * @see hook_group_config_info_alter()
+ * @see \Drupal\group\Annotation\GroupConfigEnabler
+ * @see \Drupal\group\Plugin\GroupConfigEnablerInterface
+ * @see \Drupal\group\Plugin\GroupConfigEnablerBase
+ * @see plugin_api
+ */
+class GroupConfigEnablerManager extends DefaultPluginManager implements GroupConfigEnablerManagerInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The group type storage handler.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
+   */
+  protected $groupTypeStorage;
+
+  /**
+   * A group config type storage handler.
+   *
+   * @var \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface
+   */
+  protected $groupConfigTypeStorage;
+
+  /**
+   * A collection of vanilla instances of all config enabler plugins.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerCollection
+   */
+  protected $allPlugins;
+
+  /**
+   * An list each group type's installed plugins as plugin collections.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerCollection[]
+   */
+  protected $groupTypeInstalled = [];
+
+  /**
+   * An static cache of group config type IDs per plugin ID.
+   *
+   * @var array[]
+   */
+  protected $pluginGroupConfigTypeMap;
+
+  /**
+   * The cache key for the group config type IDs per plugin ID map.
+   *
+   * @var string
+   */
+  protected $pluginGroupConfigTypeMapCacheKey;
+
+  /**
+   * An static cache of plugin IDs per group type ID.
+   *
+   * @var array[]
+   */
+  protected $groupTypePluginMap;
+
+  /**
+   * The cache key for the plugin IDs per group type ID map.
+   *
+   * @var string
+   */
+  protected $groupTypePluginMapCacheKey;
+
+  /**
+   * Constructs a GroupConfigEnablerManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct('Plugin/GroupConfigEnabler', $namespaces, $module_handler, 'Drupal\group\Plugin\GroupConfigEnablerInterface', 'Drupal\group\Annotation\GroupConfigEnabler');
+    $this->alterInfo('group_config_info');
+    $this->setCacheBackend($cache_backend, 'group_config_enablers');
+    $this->entityTypeManager = $entity_type_manager;
+    $this->pluginGroupConfigTypeMapCacheKey = $this->cacheKey . '_GCT_map';
+    $this->groupTypePluginMapCacheKey = $this->cacheKey . '_GT_map';
+  }
+
+  /**
+   * Returns the group type storage handler.
+   *
+   * @return \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
+   */
+  protected function getGroupTypeStorage() {
+    if (!isset($this->groupTypeStorage)) {
+      $this->groupTypeStorage = $this->entityTypeManager->getStorage('group_type');
+    }
+    return $this->groupTypeStorage;
+  }
+
+  /**
+   * Returns the group config type storage handler.
+   *
+   * @return \Drupal\group\Entity\Storage\GroupConfigTypeStorageInterface
+   */
+  protected function getGroupConfigTypeStorage() {
+    if (!isset($this->groupConfigTypeStorage)) {
+      $this->groupConfigTypeStorage = $this->entityTypeManager->getStorage('group_config_type');
+    }
+    return $this->groupConfigTypeStorage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAll() {
+    if (!isset($this->allPlugins)) {
+      $collection = new GroupConfigEnablerCollection($this, []);
+
+      // Add every known plugin to the collection with a vanilla configuration.
+      foreach ($this->getDefinitions() as $plugin_id => $plugin_info) {
+        $collection->setInstanceConfiguration($plugin_id, ['id' => $plugin_id]);
+      }
+
+      // Sort and set the plugin collection.
+      $this->allPlugins = $collection->sort();
+    }
+
+    return $this->allPlugins;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInstalled(GroupTypeInterface $group_type = NULL) {
+    return !isset($group_type)
+      ? $this->getVanillaInstalled()
+      : $this->getGroupTypeInstalled($group_type);
+  }
+
+  /**
+   * Retrieves a vanilla instance of every installed plugin.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerCollection
+   *   A plugin collection with a vanilla instance of every installed plugin.
+   */
+  protected function getVanillaInstalled() {
+    // Retrieve a vanilla instance of all known config enabler plugins.
+    $plugins = clone $this->getAll();
+
+    // Retrieve all installed config enabler plugin IDs.
+    $installed = $this->getInstalledIds();
+
+    // Remove uninstalled plugins from the collection.
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerCollection $plugins */
+    foreach ($plugins as $plugin_id => $plugin) {
+      if (!in_array($plugin_id, $installed)) {
+        $plugins->removeInstanceId($plugin_id);
+      }
+    }
+
+    return $plugins;
+  }
+
+  /**
+   * Retrieves fully instantiated plugins for a group type.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   The group type to instantiate the installed plugins for.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerCollection
+   *   A plugin collection with fully instantiated plugins for the group type.
+   */
+  protected function getGroupTypeInstalled(GroupTypeInterface $group_type) {
+    if (!isset($this->groupTypeInstalled[$group_type->id()])) {
+      $configurations = [];
+      $group_config_types = $this->getGroupConfigTypeStorage()->loadByGroupType($group_type);
+
+      // Get the plugin config from every group config type for the group type.
+      foreach ($group_config_types as $group_config_type) {
+        $plugin_id = $group_config_type->getConfigPluginId();
+
+        // Grab the plugin config from every group config type and amend it
+        // with the group type ID so the plugin knows what group type to use. We
+        // also specify the 'id' key because DefaultLazyPluginCollection throws
+        // an exception if it is not present.
+        $configuration = $group_config_type->get('plugin_config');
+        $configuration['group_type_id'] = $group_type->id();
+        $configuration['id'] = $plugin_id;
+
+        $configurations[$plugin_id] = $configuration;
+      }
+
+      $plugins = new GroupConfigEnablerCollection($this, $configurations);
+      $plugins->sort();
+
+      $this->groupTypeInstalled[$group_type->id()] = $plugins;
+    }
+
+    return $this->groupTypeInstalled[$group_type->id()];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInstalledIds(GroupTypeInterface $group_type = NULL) {
+    // If no group type was provided, we can find all installed plugin IDs by
+    // grabbing the keys from the group config type IDs per plugin ID map.
+    if (!isset($group_type)) {
+      return array_keys($this->getPluginGroupConfigTypeMap());
+    }
+
+    // Otherwise, we can find the entry in the plugin IDs per group type ID map.
+    $map = $this->getGroupTypePluginMap();
+    return isset($map[$group_type->id()]) ? $map[$group_type->id()] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedInstalledIds() {
+    $this->clearCachedPluginMaps();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function installEnforced(GroupTypeInterface $group_type = NULL) {
+    $enforced = [];
+
+    // Gather the ID of all plugins that are marked as enforced.
+    foreach ($this->getDefinitions() as $plugin_id => $plugin_info) {
+      if ($plugin_info['enforced']) {
+        $enforced[] = $plugin_id;
+      }
+    }
+
+    // If no group type was specified, we check all of them.
+    /** @var \Drupal\group\Entity\GroupTypeInterface[] $group_types */
+    $group_types = empty($group_type) ? $this->getGroupTypeStorage()->loadMultiple() : [$group_type];
+
+    // Search through all of the enforced plugins and install new ones.
+    foreach ($group_types as $group_type) {
+      $installed = $this->getInstalledIds($group_type);
+
+      foreach ($enforced as $plugin_id) {
+        if (!in_array($plugin_id, $installed)) {
+          $this->getGroupConfigTypeStorage()->createFromPlugin($group_type, $plugin_id)->save();
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupConfigTypeIds($plugin_id) {
+    $map = $this->getPluginGroupConfigTypeMap();
+    return isset($map[$plugin_id]) ? $map[$plugin_id] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginGroupConfigTypeMap() {
+    $map = $this->getCachedPluginGroupConfigTypeMap();
+
+    if (!isset($map)) {
+      $map = [];
+
+      /** @var \Drupal\group\Entity\GroupConfigTypeInterface[] $group_config_types */
+      $group_config_types = $this->getGroupConfigTypeStorage()->loadMultiple();
+      foreach ($group_config_types as $group_config_type) {
+        $map[$group_config_type->getConfigPluginId()][] = $group_config_type->id();
+      }
+
+      $this->setCachedPluginGroupConfigTypeMap($map);
+    }
+
+    return $map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedGroupConfigTypeIdMap() {
+    $this->clearCachedPluginMaps();
+  }
+
+  /**
+   * Returns the cached group config type ID map.
+   *
+   * @return array|null
+   *   On success this will return the group config ID map (array). On failure
+   *   this should return NULL, indicating to other methods that this has not
+   *   yet been defined. Success with no values should return as an empty array.
+   */
+  protected function getCachedPluginGroupConfigTypeMap() {
+    if (!isset($this->pluginGroupConfigTypeMap) && $cache = $this->cacheGet($this->pluginGroupConfigTypeMapCacheKey)) {
+      $this->pluginGroupConfigTypeMap = $cache->data;
+    }
+    return $this->pluginGroupConfigTypeMap;
+  }
+
+  /**
+   * Sets a cache of the group config type ID map.
+   *
+   * @param array $map
+   *   The group config type ID map to store in cache.
+   */
+  protected function setCachedPluginGroupConfigTypeMap($map) {
+    $this->cacheSet($this->pluginGroupConfigTypeMapCacheKey, $map, Cache::PERMANENT);
+    $this->pluginGroupConfigTypeMap = $map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupTypePluginMap() {
+    $map = $this->getCachedGroupTypePluginMap();
+
+    if (!isset($map)) {
+      $map = [];
+
+      /** @var \Drupal\group\Entity\GroupConfigTypeInterface[] $group_config_types */
+      $group_config_types = $this->getGroupConfigTypeStorage()->loadMultiple();
+      foreach ($group_config_types as $group_config_type) {
+        $map[$group_config_type->getGroupTypeId()][] = $group_config_type->getConfigPluginId();
+      }
+
+      $this->setCachedGroupTypePluginMap($map);
+    }
+
+    return $map;
+  }
+
+  /**
+   * Returns the cached group type plugin map.
+   *
+   * @return array|null
+   *   On success this will return the group type plugin map (array). On failure
+   *   this should return NULL, indicating to other methods that this has not
+   *   yet been defined. Success with no values should return as an empty array.
+   */
+  protected function getCachedGroupTypePluginMap() {
+    if (!isset($this->groupTypePluginMap) && $cache = $this->cacheGet($this->groupTypePluginMapCacheKey)) {
+      $this->groupTypePluginMap = $cache->data;
+    }
+    return $this->groupTypePluginMap;
+  }
+
+  /**
+   * Sets a cache of the group type plugin map.
+   *
+   * @param array $map
+   *   The group type plugin map to store in cache.
+   */
+  protected function setCachedGroupTypePluginMap($map) {
+    $this->cacheSet($this->groupTypePluginMapCacheKey, $map, Cache::PERMANENT);
+    $this->groupTypePluginMap = $map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedGroupTypeCollections(GroupTypeInterface $group_type = NULL) {
+    if (!isset($group_type)) {
+      $this->groupTypeInstalled = [];
+    }
+    else {
+      $this->groupTypeInstalled[$group_type->id()] = NULL;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedPluginMaps() {
+    if ($this->cacheBackend) {
+      $this->cacheBackend->delete($this->pluginGroupConfigTypeMapCacheKey);
+      $this->cacheBackend->delete($this->groupTypePluginMapCacheKey);
+    }
+    $this->pluginGroupConfigTypeMap = NULL;
+    $this->groupTypePluginMap = NULL;
+
+    // Also clear the array of per group type plugin collections as it shares
+    // its cache clearing requirements with the group type plugin map.
+    $this->groupTypeInstalled = [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedDefinitions() {
+    parent::clearCachedDefinitions();
+
+    // The collection of all plugins should only change if the plugin
+    // definitions change, so we can safely reset that here.
+    $this->allPlugins = NULL;
+  }
+
+}
diff --git a/src/Plugin/GroupConfigEnablerManagerInterface.php b/src/Plugin/GroupConfigEnablerManagerInterface.php
new file mode 100644
index 0000000..79ccf5a
--- /dev/null
+++ b/src/Plugin/GroupConfigEnablerManagerInterface.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\group\Plugin;
+
+use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\group\Entity\GroupTypeInterface;
+
+/**
+ * Provides a common interface for group config enabler managers.
+ */
+interface GroupConfigEnablerManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface {
+
+  /**
+   * Returns a plugin collection of all available config enablers.
+   *
+   * This collection will not have anything set in the individual plugins'
+   * configuration. Do not use any methods on the plugin that require a group
+   * type to be set or you may encounter unexpected behavior. Instead, use
+   * ::getInstalled() while providing a group type argument to get fully
+   * configured instances of the plugins.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerCollection
+   *   A plugin collection with a vanilla instance of every known plugin.
+   */
+  public function getAll();
+
+  /**
+   * Returns a plugin collection of all installed config enablers.
+   *
+   * Warning: When called without a $group_type argument, this will return a
+   * collection of vanilla plugin instances. See ::getAll() for details about
+   * vanilla instances.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   (optional) The group type to retrieve installed plugin for.
+   *
+   * @return \Drupal\group\Plugin\GroupConfigEnablerCollection
+   *   A plugin collection with a vanilla instance of every installed plugin. If
+   *   $group_type was provided, the collection will contain fully instantiated
+   *   plugins for the provided group type.
+   */
+  public function getInstalled(GroupTypeInterface $group_type = NULL);
+
+  /**
+   * Returns the plugin ID of all config enablers in use.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   (optional) The group type to retrieve plugin IDs for.
+   *
+   * @return string[]
+   *   A list of all installed config enabler plugin IDs. If $group_type was
+   *   provided, this will only return the installed IDs for that group type.
+   */
+  public function getInstalledIds(GroupTypeInterface $group_type = NULL);
+
+  /**
+   * Clears static and persistent installed plugin ID caches.
+   *
+   * @deprecated in Group 1.0-beta3, will be removed before Group 1.0-rc1. Use
+   *   ::clearCachedPluginMaps() instead.
+   */
+  public function clearCachedInstalledIds();
+
+  /**
+   * Installs all plugins which are marked as enforced.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   (optional) The group type to install enforced plugins on. Leave blank to
+   *   run the installation process for all group types.
+   */
+  public function installEnforced(GroupTypeInterface $group_type = NULL);
+
+  /**
+   * Retrieves all of the group config type IDs for a config plugin.
+   *
+   * @param $plugin_id
+   *   The ID of the plugin to retrieve group config type IDs for.
+   *
+   * @return string[]
+   *   An array of group config type IDs.
+   */
+  public function getGroupConfigTypeIds($plugin_id);
+
+  /**
+   * Retrieves a list of group config type IDs per plugin ID.
+   *
+   * @return array
+   *   An array of group config type ID arrays, keyed by plugin ID.
+   */
+  public function getPluginGroupConfigTypeMap();
+
+  /**
+   * Clears static and persistent group config type ID map caches.
+   *
+   * @deprecated in Group 1.0-beta3, will be removed before Group 1.0-rc1. Use
+   *   ::clearCachedPluginMaps() instead.
+   */
+  public function clearCachedGroupConfigTypeIdMap();
+
+  /**
+   * Retrieves a list of plugin IDs per group type ID.
+   *
+   * @return array
+   *   An array of config plugin ID arrays, keyed by group type ID.
+   */
+  public function getGroupTypePluginMap();
+
+  /**
+   * Clears the static per group type plugin collection cache.
+   *
+   * @param \Drupal\group\Entity\GroupTypeInterface $group_type
+   *   (optional) The group type to clear the cache for. Leave blank to clear
+   *   the cache for all group types.
+   */
+  public function clearCachedGroupTypeCollections(GroupTypeInterface $group_type = NULL);
+
+  /**
+   * Clears static and persistent plugin ID map caches.
+   */
+  public function clearCachedPluginMaps();
+
+}
diff --git a/src/Plugin/Validation/Constraint/GroupConfigCardinality.php b/src/Plugin/Validation/Constraint/GroupConfigCardinality.php
new file mode 100644
index 0000000..dc9afa1
--- /dev/null
+++ b/src/Plugin/Validation/Constraint/GroupConfigCardinality.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\group\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks the cardinality limits for a piece of group config.
+ *
+ * Config enabler plugins may limit the amount of times a single config entity
+ * can be added to a group as well as the amount of groups that single entity
+ * can be added to. This constraint will enforce that behavior.
+ *
+ * @Constraint(
+ *   id = "GroupConfigCardinality",
+ *   label = @Translation("Group config cardinality check", context = "Validation"),
+ *   type = "entity:group_config"
+ * )
+ */
+class GroupConfigCardinality extends Constraint {
+
+  /**
+   * The message to show when an entity has reached the group cardinality.
+   *
+   * @var string
+   */
+  public $groupMessage = '@field: %config has reached the maximum amount of groups it can be added to';
+
+  /**
+   * The message to show when an entity has reached the entity cardinality.
+   *
+   * @var string
+   */
+  public $entityMessage = '@field: %config has reached the maximum amount of times it can be added to %group';
+
+}
diff --git a/src/Plugin/Validation/Constraint/GroupConfigCardinalityValidator.php b/src/Plugin/Validation/Constraint/GroupConfigCardinalityValidator.php
new file mode 100644
index 0000000..66457b9
--- /dev/null
+++ b/src/Plugin/Validation/Constraint/GroupConfigCardinalityValidator.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\group\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Checks the amount of times a single config entity can be added to a group.
+ */
+class GroupConfigCardinalityValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * Type-hinting in parent Symfony class is off, let's fix that.
+   *
+   * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
+   */
+  protected $context;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a GroupConfigCardinalityValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($group_config, Constraint $constraint) {
+    /** @var \Drupal\group\Entity\GroupConfigInterface $group_config */
+    /** @var \Drupal\group\Plugin\Validation\Constraint\GroupConfigCardinality $constraint */
+    if (!isset($group_config)) {
+      return;
+    }
+
+    // Only run our checks if a group was referenced.
+    if (!$group = $group_config->getGroup()) {
+      return;
+    }
+
+    // Only run our checks if an entity was referenced.
+    if (!$entity = $group_config->getEntity()) {
+      return;
+    }
+
+    // Get the plugin for the group config entity.
+    $plugin = $group_config->getConfigPlugin();
+
+    // Get the cardinality settings from the plugin.
+    $group_cardinality = $plugin->getGroupCardinality();
+    $entity_cardinality = $plugin->getEntityCardinality();
+
+    // Exit early if both cardinalities are set to unlimited.
+    if ($group_cardinality <= 0 && $entity_cardinality <= 0) {
+      return;
+    }
+
+    // Get the entity_id field label for error messages.
+    $field_name = $group_config->getFieldDefinition('entity_id')->getLabel();
+
+    // Enforce the group cardinality if it's not set to unlimited.
+    if ($group_cardinality > 0) {
+      // Get the group config entities for this piece of config.
+      $properties = ['type' => $plugin->getConfigTypeConfigId(), 'entity_id' => $entity->id()];
+      $group_instances = $this->entityTypeManager
+        ->getStorage('group_config')
+        ->loadByProperties($properties);
+
+      // Get the groups this config entity already belongs to, not counting
+      // the current group towards the limit.
+      $group_ids = [];
+      foreach ($group_instances as $instance) {
+        /** @var \Drupal\group\Entity\GroupConfigInterface $instance */
+        if ($instance->getGroup()->id() != $group->id()) {
+          $group_ids[] = $instance->getGroup()->id();
+        }
+      }
+      $group_count = count(array_unique($group_ids));
+
+      // Raise a violation if the config has reached the cardinality limit.
+      if ($group_count >= $group_cardinality) {
+        $this->context->buildViolation($constraint->groupMessage)
+          ->setParameter('@field', $field_name)
+          ->setParameter('%config', $entity->label())
+          // We manually flag the entity reference field as the source of the
+          // violation so form API will add a visual indicator of where the
+          // validation failed.
+          ->atPath('entity_id.0')
+          ->addViolation();
+      }
+    }
+
+    // Enforce the entity cardinality if it's not set to unlimited.
+    if ($entity_cardinality > 0) {
+      // Get the current instances of this config entity in the group.
+      $entity_instances = $group->getConfigByEntityId($plugin->getPluginId(), $entity->id());
+      $entity_count = count($entity_instances);
+
+      // If the current group config entity has an ID, exclude that one.
+      if ($group_config_id = $group_config->id()) {
+        foreach ($entity_instances as $instance) {
+          /** @var \Drupal\group\Entity\GroupConfigInterface $instance */
+          if ($instance->id() == $group_config_id) {
+            $entity_count--;
+            break;
+          }
+        }
+      }
+
+      // Raise a violation if the config has reached the cardinality limit.
+      if ($entity_count >= $entity_cardinality) {
+        $this->context->buildViolation($constraint->entityMessage)
+          ->setParameter('@field', $field_name)
+          ->setParameter('%config', $entity->label())
+          ->setParameter('%group', $group->label())
+          // We manually flag the entity reference field as the source of the
+          // violation so form API will add a visual indicator of where the
+          // validation failed.
+          ->atPath('entity_id.0')
+          ->addViolation();
+      }
+    }
+  }
+
+}
diff --git a/src/Plugin/views/relationship/GroupConfigToEntity.php b/src/Plugin/views/relationship/GroupConfigToEntity.php
new file mode 100644
index 0000000..19349d4
--- /dev/null
+++ b/src/Plugin/views/relationship/GroupConfigToEntity.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\group\Plugin\views\relationship;
+
+/**
+ * A relationship handler for group config entity references.
+ *
+ * Definition items:
+ * - target_entity_type: The ID of the entity type this relationship maps to.
+ *
+ * @ingroup views_relationship_handlers
+ *
+ * @ViewsRelationship("group_config_to_entity")
+ */
+class GroupConfigToEntity extends GroupConfigToEntityBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTargetEntityType() {
+    return $this->definition['target_entity_type'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getJoinFieldType() {
+    return 'left_field';
+  }
+
+}
diff --git a/src/Plugin/views/relationship/GroupConfigToEntityBase.php b/src/Plugin/views/relationship/GroupConfigToEntityBase.php
new file mode 100644
index 0000000..0aafff7
--- /dev/null
+++ b/src/Plugin/views/relationship/GroupConfigToEntityBase.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Drupal\group\Plugin\views\relationship;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\group\Entity\GroupConfigType;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
+use Drupal\views\Plugin\ViewsHandlerManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A relationship handler base for group config entity references.
+ */
+abstract class GroupConfigToEntityBase extends RelationshipPluginBase {
+
+  /**
+   * The Views join plugin manager.
+   *
+   * @var \Drupal\views\Plugin\ViewsHandlerManager
+   */
+  protected $joinManager;
+
+  /**
+   * The group config enabler plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs an GroupConfigToEntityBase object.
+   *
+   * @param \Drupal\views\Plugin\ViewsHandlerManager $join_manager
+   *   The views plugin join manager.
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config enabler plugin manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ViewsHandlerManager $join_manager, GroupConfigEnablerManagerInterface $plugin_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->joinManager = $join_manager;
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('plugin.manager.views.join'),
+      $container->get('plugin.manager.group_config_enabler')
+    );
+  }
+
+  /**
+   * Retrieves the entity type ID this plugin targets.
+   *
+   * Do not return 'group_config', but the actual entity type ID you're trying
+   * to link up to the group_config entity type.
+   *
+   * @return string
+   *   The target entity type ID.
+   */
+  abstract protected function getTargetEntityType();
+
+  /**
+   * Retrieves type of join field to use.
+   *
+   * Can be either 'field' or 'left_field'.
+   *
+   * @return string
+   *   The type of join field to use.
+   */
+  abstract protected function getJoinFieldType();
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['group_config_plugins']['default'] = [];
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    // Retrieve all of the plugins that can serve this entity type.
+    $options = [];
+    foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) {
+      /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+      if ($plugin->getEntityTypeId() === $this->getTargetEntityType()) {
+        $options[$plugin_id] = $plugin->getLabel();
+      }
+    }
+
+    $form['group_config_plugins'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Filter by plugin'),
+      '#description' => $this->t('Refine the result by plugin. Leave empty to select all plugins, including those that could be added after this relationship was configured.'),
+      '#options' => $options,
+      '#weight' => -2,
+      '#default_value' => $this->options['group_config_plugins'],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    $this->ensureMyTable();
+
+    // Build the join definition.
+    $def = $this->definition;
+    $def['table'] = $this->definition['base'];
+    $def['field'] = $this->definition['base field'];
+    $def['left_table'] = $this->tableAlias;
+    $def['left_field'] = $this->realField;
+    $def['adjusted'] = TRUE;
+
+    // Change the join to INNER if the relationship is required.
+    if (!empty($this->options['required'])) {
+      $def['type'] = 'INNER';
+    }
+
+    // If there were extra join conditions added in the definition, use them.
+    if (!empty($this->definition['extra'])) {
+      $def['extra'] = $this->definition['extra'];
+    }
+
+    // We can't run an IN-query on an empty array. So if there are no group
+    // config types yet, we need to make sure the JOIN does not return any GCT
+    // that does not serve the entity type that was configured for this handler
+    // instance.
+    $group_config_type_ids = $this->getGroupConfigTypeIds();
+    if (empty($group_config_type_ids)) {
+      $group_config_type_ids = ['***'];
+    }
+
+    // Then add our own join condition, namely the group config type IDs.
+    $def['extra'][] = [
+      $this->getJoinFieldType() => 'type',
+      'value' => $group_config_type_ids,
+    ];
+
+    // Use the standard join plugin unless instructed otherwise.
+    $join_id = !empty($def['join_id']) ? $def['join_id'] : 'standard';
+    $join = $this->joinManager->createInstance($join_id, $def);
+
+    // Add the join using a more verbose alias.
+    $alias = $def['table'] . '_' . $this->table;
+    $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship);
+
+    // Add access tags if the base table provides it.
+    $table_data = $this->viewsData->get($def['table']);
+    if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) {
+      $access_tag = $table_data['table']['base']['access query tag'];
+      $this->query->addTag($access_tag);
+    }
+  }
+
+  /**
+   * Returns the group config types this relationship should filter on.
+   *
+   * This checks if any plugins were selected on the option form and, in that
+   * case, loads only those group config types available to the selected
+   * plugins. Otherwise, all possible group config types for the relationship's
+   * entity type are loaded.
+   *
+   * This needs to happen live to cover the use case where a group config
+   * plugin is installed on a group type after this relationship has been
+   * configured on a view without any plugins selected.
+   *
+   * @todo Could be cached even more, I guess.
+   *
+   * @return string[]
+   *   The group config type IDs to filter on.
+   */
+  protected function getGroupConfigTypeIds() {
+    $plugin_ids = array_filter($this->options['group_config_plugins']);
+
+    $group_config_type_ids = [];
+    foreach ($plugin_ids as $plugin_id) {
+      $group_config_type_ids = array_merge($group_config_type_ids, $this->pluginManager->getGroupConfigTypeIds($plugin_id));
+    }
+
+    return $plugin_ids ? $group_config_type_ids : array_keys(GroupConfigType::loadByEntityTypeId($this->getTargetEntityType()));
+  }
+
+}
diff --git a/src/Plugin/views/relationship/GroupConfigToEntityReverse.php b/src/Plugin/views/relationship/GroupConfigToEntityReverse.php
new file mode 100644
index 0000000..50de7be
--- /dev/null
+++ b/src/Plugin/views/relationship/GroupConfigToEntityReverse.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\group\Plugin\views\relationship;
+
+/**
+ * A relationship handler which reverses group config entity references.
+ *
+ * @ingroup views_relationship_handlers
+ *
+ * @ViewsRelationship("group_config_to_entity_reverse")
+ */
+class GroupConfigToEntityReverse extends GroupConfigToEntityBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTargetEntityType() {
+    return $this->definition['entity_type'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getJoinFieldType() {
+    return 'field';
+  }
+
+}
diff --git a/src/Plugin/views/relationship/GroupToGroupConfig.php b/src/Plugin/views/relationship/GroupToGroupConfig.php
new file mode 100644
index 0000000..bd22fcd
--- /dev/null
+++ b/src/Plugin/views/relationship/GroupToGroupConfig.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace Drupal\group\Plugin\views\relationship;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\group\Entity\GroupConfigType;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
+use Drupal\views\Plugin\ViewsHandlerManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A relationship handler for group config.
+ *
+ * @ingroup views_relationship_handlers
+ *
+ * @ViewsRelationship("group_to_group_config")
+ */
+class GroupToGroupConfig extends RelationshipPluginBase {
+
+  /**
+   * The Views join plugin manager.
+   *
+   * @var \Drupal\views\Plugin\ViewsHandlerManager
+   */
+  protected $joinManager;
+
+  /**
+   * The group config enabler plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * The group config type IDs to filter the join on.
+   *
+   * @var string[]
+   */
+  protected $groupConfigTypeIds;
+
+  /**
+   * Constructs a GroupToGroupConfig object.
+   *
+   * @param \Drupal\views\Plugin\ViewsHandlerManager $join_manager
+   *   The views plugin join manager.
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config enabler plugin manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ViewsHandlerManager $join_manager, GroupConfigEnablerManagerInterface $plugin_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->joinManager = $join_manager;
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('plugin.manager.views.join'),
+      $container->get('plugin.manager.group_config_enabler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['group_config_plugins']['default'] = [];
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    $form['group_config_plugins'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Filter by plugin'),
+      '#description' => $this->t('Refine the result by plugin. Leave empty to select all plugins, including those that could be added after this relationship was configured.'),
+      '#options' => $this->getConfigPluginOptions(),
+      '#weight' => -2,
+      '#default_value' => $this->options['group_config_plugins'],
+    ];
+  }
+
+  /**
+   * Builds the options for the config plugin selection.
+   *
+   * @return string[]
+   *   An array of config plugin labels, keyed by plugin ID.
+   */
+  protected function getConfigPluginOptions() {
+    $options = [];
+    foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) {
+      /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+      $options[$plugin_id] = $plugin->getLabel();
+    }
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    $this->ensureMyTable();
+
+    // Build the join definition.
+    $def = $this->definition;
+    $def['table'] = $this->definition['base'];
+    $def['field'] = $this->definition['base field'];
+    $def['left_table'] = $this->tableAlias;
+    $def['left_field'] = $this->realField;
+    $def['adjusted'] = TRUE;
+
+    // Change the join to INNER if the relationship is required.
+    if (!empty($this->options['required'])) {
+      $def['type'] = 'INNER';
+    }
+
+    // If there were extra join conditions added in the definition, use them.
+    if (!empty($this->definition['extra'])) {
+      $def['extra'] = $this->definition['extra'];
+    }
+
+    // We can't run an IN-query on an empty array. So if there are no group
+    // config types yet, we do not add our extra condition to the JOIN.
+    $group_config_type_ids = $this->getGroupConfigTypeIds();
+    if (!empty($group_config_type_ids)) {
+      $def['extra'][] = [
+        'field' => 'type',
+        'value' => $group_config_type_ids,
+      ];
+    }
+
+    // Use the standard join plugin unless instructed otherwise.
+    $join_id = !empty($def['join_id']) ? $def['join_id'] : 'standard';
+    $join = $this->joinManager->createInstance($join_id, $def);
+
+    // Add the join using a more verbose alias.
+    $alias = $def['table'] . '_' . $this->table;
+    $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship);
+
+    // Add access tags if the base table provides it.
+    $table_data = $this->viewsData->get($def['table']);
+    if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) {
+      $access_tag = $table_data['table']['base']['access query tag'];
+      $this->query->addTag($access_tag);
+    }
+  }
+
+  /**
+   * Returns the group config types this relationship should filter on.
+   *
+   * This checks if any plugins were selected on the option form and, in that
+   * case, loads only those group config types available to the selected
+   * plugins. Otherwise, all possible group config types for the relationship's
+   * entity type are loaded.
+   *
+   * This needs to happen live to cover the use case where a group config
+   * plugin is installed on a group type after this relationship has been
+   * configured on a view without any plugins selected.
+   *
+   * @return string[]
+   *   The group config type IDs to filter on.
+   */
+  protected function getGroupConfigTypeIds() {
+    // Even though the retrieval needs to happen live, there's nothing stopping
+    // us from statically caching it during runtime.
+    if (!isset($this->groupConfigTypeIds)) {
+      $plugin_ids = array_filter($this->options['group_config_plugins']);
+
+      $group_config_type_ids = [];
+      foreach ($plugin_ids as $plugin_id) {
+        $group_config_type_ids = array_merge($group_config_type_ids, $this->pluginManager->getGroupConfigTypeIds($plugin_id));
+      }
+
+      $this->groupConfigTypeIds = $plugin_ids
+        ? $group_config_type_ids
+        : array_keys(GroupConfigType::loadMultiple());
+    }
+
+    return $this->groupConfigTypeIds;
+  }
+
+}
diff --git a/src/ProxyClass/UninstallValidator/GroupConfigUninstallValidator.php b/src/ProxyClass/UninstallValidator/GroupConfigUninstallValidator.php
new file mode 100644
index 0000000..b524ec1
--- /dev/null
+++ b/src/ProxyClass/UninstallValidator/GroupConfigUninstallValidator.php
@@ -0,0 +1,88 @@
+<?php
+// @codingStandardsIgnoreFile
+
+/**
+ * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\group\UninstallValidator\GroupConfigUninstallValidator' "modules/contrib/group/src".
+ */
+
+namespace Drupal\group\ProxyClass\UninstallValidator {
+
+    /**
+     * Provides a proxy class for \Drupal\group\UninstallValidator\GroupConfigUninstallValidator.
+     *
+     * @see \Drupal\Component\ProxyBuilder
+     */
+    class GroupConfigUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
+    {
+
+        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+        /**
+         * The id of the original proxied service.
+         *
+         * @var string
+         */
+        protected $drupalProxyOriginalServiceId;
+
+        /**
+         * The real proxied service, after it was lazy loaded.
+         *
+         * @var \Drupal\group\UninstallValidator\GroupConfigUninstallValidator
+         */
+        protected $service;
+
+        /**
+         * The service container.
+         *
+         * @var \Symfony\Component\DependencyInjection\ContainerInterface
+         */
+        protected $container;
+
+        /**
+         * Constructs a ProxyClass Drupal proxy object.
+         *
+         * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+         *   The container.
+         * @param string $drupal_proxy_original_service_id
+         *   The service ID of the original service.
+         */
+        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        {
+            $this->container = $container;
+            $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
+        }
+
+        /**
+         * Lazy loads the real service from the container.
+         *
+         * @return object
+         *   Returns the constructed real service.
+         */
+        protected function lazyLoadItself()
+        {
+            if (!isset($this->service)) {
+                $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
+            }
+
+            return $this->service;
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function validate($module)
+        {
+            return $this->lazyLoadItself()->validate($module);
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
+        {
+            return $this->lazyLoadItself()->setStringTranslation($translation);
+        }
+
+    }
+
+}
diff --git a/src/UninstallValidator/GroupConfigUninstallValidator.php b/src/UninstallValidator/GroupConfigUninstallValidator.php
new file mode 100644
index 0000000..650386d
--- /dev/null
+++ b/src/UninstallValidator/GroupConfigUninstallValidator.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\group\UninstallValidator;
+
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\group\Entity\GroupConfigType;
+use Drupal\group\Plugin\GroupConfigEnablerManagerInterface;
+
+/**
+ *
+ */
+class GroupConfigUninstallValidator implements ModuleUninstallValidatorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The query factory to create entity queries.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $queryFactory;
+
+  /**
+   * The group config plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupConfigEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a new GroupConfigUninstallValidator object.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
+   *   The entity query object.
+   * @param \Drupal\group\Plugin\GroupConfigEnablerManagerInterface $plugin_manager
+   *   The group config plugin manager.
+   */
+  public function __construct(TranslationInterface $string_translation, QueryFactory $query_factory, GroupConfigEnablerManagerInterface $plugin_manager) {
+    $this->stringTranslation = $string_translation;
+    $this->queryFactory = $query_factory;
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($module) {
+    $reasons = $plugin_names = [];
+
+    /** @var \Drupal\group\Plugin\GroupConfigEnablerInterface $plugin */
+    foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) {
+      if ($plugin->getProvider() == $module && $this->hasGroupConfig($plugin_id)) {
+        $plugin_names[] = $plugin->getLabel();
+      }
+    }
+
+    if (!empty($plugin_names)) {
+      $reasons[] = $this->t('The following group config plugins still have config for them: %plugins.', ['%plugins' => implode(', ', $plugin_names)]);
+    }
+
+    return $reasons;
+  }
+
+  /**
+   * Determines if there is any group config for a config enabler plugin.
+   *
+   * @param string $plugin_id
+   *   The group config enabler plugin ID to check for group config.
+   *
+   * @return bool
+   *   Whether there are group config entities for the given plugin ID.
+   */
+  protected function hasGroupConfig($plugin_id) {
+    $group_config_types = array_keys(GroupConfigType::loadByConfigPluginId($plugin_id));
+
+    if (empty($group_config_types)) {
+      return FALSE;
+    }
+
+    $entity_count = $this->queryFactory->get('group_config')
+      ->condition('type', $group_config_types, 'IN')
+      ->count()
+      ->execute();
+
+    return (bool) $entity_count;
+  }
+
+}
diff --git a/templates/group-config.html.twig b/templates/group-config.html.twig
new file mode 100644
index 0000000..1ae7c1f
--- /dev/null
+++ b/templates/group-config.html.twig
@@ -0,0 +1,54 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a group config entity.
+ *
+ * Available variables:
+ * - group_config: The group config entity with limited access to object
+ *   properties and methods. Only "getter" methods (method names starting with
+ *   "get", "has", or "is") and a few common methods such as "id" and "label"
+ *   are available. Calling other methods (such as group.delete) will result in
+ *   an exception.
+ * - label: The title of the group config entity.
+ * - config: All group config items. Use {{ config }} to print them all,
+ *   or print a subset such as {{ config.field_example }}. Use
+ *   {{ config|without('field_example') }} to temporarily suppress the
+ *   printing of a given child element.
+ * - url: Direct URL of the current group config entity.
+ * - attributes: HTML attributes for the containing element.
+ *   The attributes.class element may contain one or more of the following
+ *   classes:
+ *   - group-config: The current template type (also known as a "theming hook").
+ *   - group-config--[type]: The current group config type.
+ *   - group-config--[view_mode]: The View Mode of the group config.
+ * - title_attributes: Same as attributes, except applied to the main title
+ *   tag that appears in the template.
+ * - config_attributes: Same as attributes, except applied to the main
+ *   config tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ *   displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ *   displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ *
+ * @see template_preprocess_group()
+ *
+ * @ingroup themeable
+ */
+#}
+<div{{ attributes }}>
+
+    {{ title_prefix }}
+    {% if not page %}
+        <h2{{ title_attributes }}>
+            <a href="{{ url }}" rel="bookmark">{{ label }}</a>
+        </h2>
+    {% endif %}
+    {{ title_suffix }}
+
+    <div{{ config_attributes }}>
+        {{ config }}
+    </div>
+
+</div>
