diff --git a/ctools.services.yml b/ctools.services.yml
index 046813b..eaa38b8 100644
--- a/ctools.services.yml
+++ b/ctools.services.yml
@@ -31,3 +31,8 @@ services:
   ctools.context_mapper:
     class: Drupal\ctools\ContextMapper
     arguments: ['@entity.repository']
+  ctools.serializable.tempstore.factory:
+    class: Drupal\ctools\SerializableTempstoreFactory
+    arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%user.tempstore.expire%']
+    tags:
+      - { name: backend_overridable }
diff --git a/modules/ctools_inline_block/ctools_inline_block.info.yml b/modules/ctools_inline_block/ctools_inline_block.info.yml
new file mode 100644
index 0000000..5765535
--- /dev/null
+++ b/modules/ctools_inline_block/ctools_inline_block.info.yml
@@ -0,0 +1,9 @@
+name: 'Inline Blocks'
+type: module
+description: 'Exposes the custom block functionality of Drupal core as inline blocks what do not save to the database.'
+package: 'Chaos tool suite'
+version: 3.x
+core: 8.x
+dependencies:
+    - inline_entity_form
+    - block_content
diff --git a/modules/ctools_inline_block/ctools_inline_block.install b/modules/ctools_inline_block/ctools_inline_block.install
new file mode 100644
index 0000000..81c9110
--- /dev/null
+++ b/modules/ctools_inline_block/ctools_inline_block.install
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Implements hook_schema().
+ */
+function ctools_inline_block_schema() {
+  return [
+    'inline_block' => [
+      'fields' => [
+        'uuid' => [
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+          'description' => 'UUID of the inline block.',
+        ],
+        'loader' => [
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => TRUE,
+          'description' => 'ID of the service which can load the block.',
+        ],
+        'data' => [
+          'type' => 'text',
+          'description' => 'Additional data needed to load the block.',
+        ],
+      ],
+      'primary key' => ['uuid'],
+    ],
+  ];
+}
diff --git a/modules/ctools_inline_block/ctools_inline_block.module b/modules/ctools_inline_block/ctools_inline_block.module
new file mode 100644
index 0000000..141b9c2
--- /dev/null
+++ b/modules/ctools_inline_block/ctools_inline_block.module
@@ -0,0 +1,39 @@
+<?php
+
+use Drupal\block\BlockInterface;
+use Drupal\ctools_inline_block\Plugin\Block\InlineBlock;
+
+/**
+ * Implements hook_ENTITY_TYPE_insert().
+ */
+function ctools_inline_block_block_insert(BlockInterface $block) {
+  $plugin = $block->getPlugin();
+
+  if ($plugin instanceof InlineBlock) {
+    \Drupal::database()
+      ->insert('inline_block')
+      ->fields([
+        'uuid' => $plugin->getEntity()->uuid(),
+        'loader' => 'ctools.block_loader',
+        'data' => $block->id(),
+      ])
+      ->execute();
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete().
+ */
+function ctools_inline_block_block_delete(BlockInterface $block) {
+  \Drupal::database()
+    ->delete('inline_block')
+    ->condition('loader', 'ctools.block_loader')
+    ->condition('data', $block->id())
+    ->execute();
+
+  $plugin = $block->getPlugin();
+
+  if ($plugin instanceof InlineBlock) {
+    $plugin->getEntity()->delete();
+  }
+}
diff --git a/modules/ctools_inline_block/ctools_inline_block.services.yml b/modules/ctools_inline_block/ctools_inline_block.services.yml
new file mode 100644
index 0000000..3eed444
--- /dev/null
+++ b/modules/ctools_inline_block/ctools_inline_block.services.yml
@@ -0,0 +1,5 @@
+services:
+  ctools.block_loader:
+    class: '\Drupal\ctools_inline_block\BlockLoader'
+    arguments:
+      - '@entity_type.manager'
diff --git a/modules/ctools_inline_block/src/BlockLoader.php b/modules/ctools_inline_block/src/BlockLoader.php
new file mode 100644
index 0000000..40d413b
--- /dev/null
+++ b/modules/ctools_inline_block/src/BlockLoader.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\ctools_inline_block;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+class BlockLoader implements BlockLoaderInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * BlockLoader constructor.
+   *
+   * @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 function load($uuid, $data = NULL) {
+    // $data is expected to be the ID of the block config entity.
+    if ($data) {
+      return $this->entityTypeManager->getStorage('block')->load($data)->getPlugin()->getEntity();
+    }
+    else {
+      throw new \InvalidArgumentException('No block entity ID was specified');
+    }
+  }
+
+}
diff --git a/modules/ctools_inline_block/src/BlockLoaderInterface.php b/modules/ctools_inline_block/src/BlockLoaderInterface.php
new file mode 100644
index 0000000..526c064
--- /dev/null
+++ b/modules/ctools_inline_block/src/BlockLoaderInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\ctools_inline_block;
+
+/**
+ * Defines an interface for loading inline blocks in a standard way.
+ */
+interface BlockLoaderInterface {
+
+  /**
+   * Loads an inline block by UUID.
+   *
+   * @param string $uuid
+   *   The inline block UUID.
+   * @param mixed $data
+   *   (optional) Additional data needed to load the block.
+   *
+   * @return \Drupal\ctools_inline_block\Entity\BlockContent
+   *   The inline block, or NULL if it could not be loaded.
+   */
+  public function load($uuid, $data = NULL);
+
+}
diff --git a/modules/ctools_inline_block/src/Entity/BlockContent.php b/modules/ctools_inline_block/src/Entity/BlockContent.php
new file mode 100644
index 0000000..3deb237
--- /dev/null
+++ b/modules/ctools_inline_block/src/Entity/BlockContent.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\ctools_inline_block\Entity;
+
+use Drupal\block_content\Entity\BlockContent as CoreBlockContent;
+
+/**
+ * Defines the custom block entity class.
+ *
+ * @ContentEntityType(
+ *   id = "inline_block_content",
+ *   label = @Translation("Custom block"),
+ *   bundle_label = @Translation("Custom block type"),
+ *   handlers = {
+ *     "storage" = "Drupal\ctools_inline_block\Entity\InlineBlockStorage",
+ *     "access" = "Drupal\block_content\BlockContentAccessControlHandler",
+ *     "list_builder" = "Drupal\block_content\BlockContentListBuilder",
+ *     "view_builder" = "Drupal\block_content\BlockContentViewBuilder",
+ *     "views_data" = "Drupal\block_content\BlockContentViewsData",
+ *     "form" = {
+ *       "add" = "Drupal\block_content\BlockContentForm",
+ *       "edit" = "Drupal\block_content\BlockContentForm",
+ *       "delete" = "Drupal\block_content\Form\BlockContentDeleteForm",
+ *       "default" = "Drupal\block_content\BlockContentForm"
+ *     },
+ *     "translation" = "Drupal\block_content\BlockContentTranslationHandler"
+ *   },
+ *   admin_permission = "administer blocks",
+ *   base_table = "block_content",
+ *   revision_table = "block_content_revision",
+ *   data_table = "block_content_field_data",
+ *   revision_data_table = "block_content_field_revision",
+ *   links = {
+ *     "canonical" = "/block/{block_content}",
+ *     "delete-form" = "/block/{block_content}/delete",
+ *     "edit-form" = "/block/{block_content}",
+ *     "collection" = "/admin/structure/block/block-content",
+ *   },
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "revision" = "revision_id",
+ *     "bundle" = "type",
+ *     "label" = "info",
+ *     "langcode" = "langcode",
+ *     "uuid" = "uuid"
+ *   },
+ *   bundle_entity_type = "block_content_type",
+ *   render_cache = FALSE,
+ * )
+ */
+class BlockContent extends CoreBlockContent {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values, $entity_type, $bundle = FALSE, array $translations = []) {
+    parent::__construct($values, 'block_content', $bundle, $translations);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->uuid();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save() {
+    return $this->entityTypeManager()
+      ->getStorage('inline_block_content')
+      ->save($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete() {
+    parent::delete();
+
+    $this->entityTypeManager()
+      ->getStorage('inline_block_content')
+      ->resetCache((array) $this->id());
+  }
+
+}
diff --git a/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php
new file mode 100644
index 0000000..fd45f8b
--- /dev/null
+++ b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\ctools_inline_block\Entity;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+
+/**
+ * A content entity storage handler that does not save to the database.
+ */
+class InlineBlockStorage extends SqlContentEntityStorage {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
+    parent::__construct(
+      $entity_manager->getDefinition('block_content'),
+      $database,
+      $entity_manager,
+      $cache,
+      $language_manager
+    );
+
+    $this->entityClass = $entity_type->getClass();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function cleanIds(array $ids) {
+    return array_map('strval', $ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getFromStorage(array $ids = NULL) {
+    $id_key = $this->idKey;
+    $this->idKey = 'uuid';
+    $entities = parent::getFromStorage($ids);
+    $this->idKey = $id_key;
+    return $entities;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildQuery($ids, $revision_id = FALSE) {
+    return $this->database
+      ->select('inline_block', 'ib')
+      ->fields('ib')
+      ->condition('uuid', $ids, 'IN');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
+    $mapped = [];
+    foreach ($records as $uuid => $record) {
+      $mapped[$uuid] = \Drupal::service($record->loader)->load($uuid, $record->data);
+    }
+    return $mapped;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $this->entityType = $entity_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = array()) {
+  }
+
+}
diff --git a/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php
new file mode 100644
index 0000000..9662b98
--- /dev/null
+++ b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\ctools_inline_block\Plugin\Block;
+
+use Drupal\block_content\Plugin\Block\BlockContentBlock;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a generic custom block type.
+ *
+ * @Block(
+ *  id = "inline_content",
+ *  admin_label = @Translation("Inline block"),
+ *  category = @Translation("Inline"),
+ *  deriver = "Drupal\ctools_inline_block\Plugin\Derivative\InlineBlock"
+ * )
+ */
+class InlineBlock extends BlockContentBlock {
+
+  /**
+   * The inline block content entity.
+   *
+   * @var \Drupal\block_content\BlockContentInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntity() {
+    if (empty($this->entity) && isset($this->configuration['entity'])) {
+      $this->entity = unserialize($this->configuration['entity']);
+    }
+    return $this->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockForm($form, FormStateInterface $form_state) {
+    $options = $this->entityManager->getViewModeOptionsByBundle('block_content', $this->getDerivativeId());
+    $form['view_mode'] = array(
+      '#type' => 'select',
+      '#options' => $options,
+      '#title' => $this->t('View mode'),
+      '#description' => $this->t('Output the block in this view mode.'),
+      '#default_value' => $this->configuration['view_mode'],
+      '#access' => (count($options) > 1),
+    );
+    $form['title']['#description'] = $this->t('The title of the block as shown to the user.');
+    $form['entity'] = [
+      '#type' => 'inline_entity_form',
+      '#entity_type' => 'inline_block_content',
+      '#bundle' => $this->getDerivativeId(),
+      // If the #default_value is NULL, a new entity will be created.
+      '#default_value' => $this->getEntity(),
+    ];
+    $form['#process'][] = [static::class, 'enableInlineEntityForm'];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    $this->configuration['view_mode'] = $form_state->getValue('view_mode');
+    /** @var \Drupal\block_content\BlockContentInterface $entity */
+    $entity = $form['settings']['entity']['#entity'] ?: $form['entity']['#entity'];
+    $this->configuration['label'] = $entity->label();
+    $this->configuration['langcode'] = $form_state->getValue('langcode');
+
+    $this->entity = $entity;
+    $this->configuration['entity'] = serialize($entity);
+  }
+
+}
diff --git a/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php
new file mode 100644
index 0000000..ba73f8d
--- /dev/null
+++ b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\ctools_inline_block\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\TranslationManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Retrieves bundle types for inline blocks.
+ */
+class InlineBlock extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The custom block type storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $blockContentTypeStorage;
+
+  /**
+   * @var \Drupal\Core\StringTranslation\TranslationManager
+   */
+  protected $translation;
+
+  /**
+   * Constructs a BlockContentType object.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $block_content_type_storage
+   *   The custom block storage.
+   */
+  public function __construct(EntityStorageInterface $block_content_type_storage, TranslationManager $translation) {
+    $this->blockContentTypeStorage = $block_content_type_storage;
+    $this->translation = $translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $entity_manager->getStorage('block_content_type'),
+      $container->get('string_translation')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $block_types = $this->blockContentTypeStorage->loadMultiple();
+    // Reset the discovered definitions.
+    $this->derivatives = [];
+    /** @var $type \Drupal\block_content\Entity\BlockContent */
+    foreach ($block_types as $type) {
+      $this->derivatives[$type->id()] = $base_plugin_definition;
+      $this->derivatives[$type->id()]['admin_label'] = $this->translation->translate('Inline @type', ['@type' => $type->label()]);
+      $this->derivatives[$type->id()]['config_dependencies']['content'] = array(
+        $type->getConfigDependencyName()
+      );
+    }
+    return $this->derivatives;
+  }
+
+}
diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml
new file mode 100644
index 0000000..02982e4
--- /dev/null
+++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml
@@ -0,0 +1,5 @@
+id: basic
+label: 'Basic block'
+revision: 0
+description: 'A basic block contains a title and a body.'
+langcode: en
diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml
new file mode 100644
index 0000000..e6accfd
--- /dev/null
+++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - block_content.type.basic
+    - field.field.block_content.basic.body
+  module:
+    - text
+id: block_content.basic.default
+targetEntityType: block_content
+bundle: basic
+mode: default
+content:
+  body:
+    type: text_default
+    weight: 0
+    label: hidden
+    settings: {  }
+    third_party_settings: {  }
+hidden: {  }
diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml
new file mode 100644
index 0000000..89118ef
--- /dev/null
+++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml
@@ -0,0 +1,21 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - block_content.type.basic
+    - field.storage.block_content.body
+  module:
+    - text
+id: block_content.basic.body
+field_name: body
+entity_type: block_content
+bundle: basic
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: {  }
+default_value_callback: ''
+settings:
+  display_summary: false
+field_type: text_with_summary
diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml
new file mode 100644
index 0000000..deb4ad3
--- /dev/null
+++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml
@@ -0,0 +1,8 @@
+type: module
+name: CTools Inline Block Test
+description: 'Required for CTools Inline Block simpletests only.'
+core: 8.x
+dependencies:
+  - ctools_inline_block
+  - block_content
+  - text
diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php
new file mode 100644
index 0000000..5f1dd50
--- /dev/null
+++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\ctools_inline_block_test\Plugin\Block;
+
+use Drupal\block_content\Plugin\Block\BlockContentBlock as BaseBlockContentBlock;
+
+/**
+ * Defines a generic custom block type.
+ *
+ * @Block(
+ *  id = "inline_block_content",
+ *  admin_label = @Translation("Custom block"),
+ *  deriver = "Drupal\block_content\Plugin\Derivative\BlockContent"
+ * )
+ */
+class BlockContentBlock extends BaseBlockContentBlock {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginId() {
+    // Masquerade as the block_content plugin.
+    return substr(parent::getPluginId(), 10);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntity() {
+    $uuid = $this->getDerivativeId();
+    if (!isset($this->blockContent)) {
+      $this->blockContent = $this->entityManager->loadEntityByUuid('inline_block_content', $uuid);
+    }
+    return $this->blockContent;
+  }
+
+}
diff --git a/modules/ctools_inline_block/tests/src/Functional/RenderTest.php b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php
new file mode 100644
index 0000000..755269c
--- /dev/null
+++ b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\ctools_inilne_block\Functional;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @group ctools_inline_block
+ */
+class RenderTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'block_content',
+    'ctools',
+    'field',
+    'field_ui',
+    'filter',
+    'panels',
+    'user',
+  ];
+
+  public function testShadowBlockRenderedMarkup() {
+    $assert = $this->assertSession();
+
+    $block_content = BlockContent::create([
+      'type' => 'basic',
+      'info' => $this->randomMachineName(16),
+      'body' => 'Yeah, I like animals better than people sometimes... Especially dogs.',
+    ]);
+    $block_content->save();
+
+    $block = $this->placeBlock('block_content:' . $block_content->uuid(), [
+      'region' => 'content',
+    ]);
+    $selector = '#block-' . $block->id();
+
+    $this->drupalGet('/user/login');
+
+    $concrete_output = $assert->elementExists('css', $selector)->getOuterHtml();
+
+    $block->set('plugin', 'panelizer_' . $block->getPluginId())->save();
+    $this->getSession()->reload();
+    $shadow_output = $assert->elementExists('css', $selector)->getOuterHtml();
+
+    $this->assertSame($concrete_output, $shadow_output);
+  }
+
+}
diff --git a/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php
new file mode 100644
index 0000000..33d0f3f
--- /dev/null
+++ b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Drupal\Tests\ctools_inline_block\Kernel;
+
+use Drupal\block_content\BlockContentInterface;
+use Drupal\block_content\Entity\BlockContent as CoreBlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\ctools_inline_block\Entity\BlockContent as InlineBlockContent;
+
+/**
+ * Tests the storage layer of inline blocks.
+ *
+ * @group ctools_inline_block
+ */
+class StorageTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'block_content',
+    'ctools_inline_block',
+    'field',
+    'field_ui',
+    'filter',
+    'text',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('block_content');
+
+    // Create a custom block type.
+    BlockContentType::create([
+      'id' => 'test_type',
+      'label' => $this->randomMachineName(),
+    ])->save();
+
+    // Create a text field on the custom block type.
+    $storage = FieldStorageConfig::create([
+      'entity_type' => 'block_content',
+      'type' => 'text',
+      'field_name' => 'body',
+    ]);
+    $storage->save();
+
+    FieldConfig::create([
+      'bundle' => 'test_type',
+      'field_storage' => $storage,
+    ])->save();
+  }
+
+  /**
+   * Tests that database tables are not created for inline blocks.
+   */
+  public function testNoShadowTables() {
+    $this->assertFalse(\Drupal::database()->schema()->tableExists('inline_block_content'));
+  }
+
+  /**
+   * Tests that inline blocks can transparently load concrete blocks.
+   */
+  public function testLoad() {
+    $original = CoreBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.',
+    ]);
+    $original->save();
+
+    $id = $original->uuid();
+
+    // Test static loading.
+    $this->assertShadow($original, InlineBlockContent::load($id));
+
+    // Test container-ey loading.
+    $shadow = \Drupal::entityTypeManager()
+      ->getStorage('inline_block_content')
+      ->load($id);
+    $this->assertShadow($original, $shadow);
+
+    // Test static multiple loading.
+    $shadows = InlineBlockContent::loadMultiple([$id]);
+    $this->assertShadow($original, reset($shadows));
+
+    // Test container-ey multiple loading.
+    $shadows = \Drupal::entityTypeManager()
+      ->getStorage('inline_block_content')
+      ->loadMultiple([$id]);
+    $this->assertShadow($original, reset($shadows));
+
+    // Test loading a shadow block using the original block's UUID.
+    $shadow = \Drupal::service('entity.repository')
+      ->loadEntityByUuid('panelizer_block_content', $original->uuid());
+    $this->assertShadow($original, $shadow);
+  }
+
+  /**
+   * Tests that shadow blocks can be created like normal custom blocks.
+   */
+  public function testCreateShadow() {
+    $values = [
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+    ];
+
+    // Test static creation.
+    $block = InlineBlockContent::create($values);
+    $this->assertInstanceOf(InlineBlockContent::class, $block);
+    $this->assertSame('block_content', $block->getEntityTypeId());
+
+    // Test container-ey creation.
+    $block = \Drupal::entityTypeManager()
+      ->getStorage('inline_block_content')
+      ->create($values);
+    $this->assertInstanceOf(InlineBlockContent::class, $block);
+    $this->assertSame('block_content', $block->getEntityTypeId());
+  }
+
+  /**
+   * @depends testCreateShadow
+   */
+  public function testNullSave() {
+    $block = InlineBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Creste di galli spaghettoni trennette pennette trennette penne zita linguettine orecchiette pappardelle gramigna casarecce.',
+    ]);
+    $block->save();
+    // Inline blocks use their UUID as their ID.
+    $this->assertEquals($block->id(), $block->uuid());
+
+    // Ensure that no custom blocks were actually created.
+    $count = \Drupal::entityQuery('block_content')
+      ->count()
+      ->execute();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    // Nothing should be written to the field tables.
+    $count = \Drupal::database()
+      ->select('block_content__body')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    $count = \Drupal::database()
+      ->select('block_content_revision__body')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    // No shadow blocks should be written into the database.
+    $count = \Drupal::entityQuery('inline_block_content')
+      ->count()
+      ->execute();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+  }
+
+  /**
+   * @depends testLoadBlockContentByShadow
+   */
+  public function testDeleteBlockContentByShadow() {
+    $original = CoreBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.',
+    ]);
+    $original->save();
+
+    $id = $original->id();
+    InlineBlockContent::load($id)->delete();
+
+    $this->assertNull(CoreBlockContent::load($id));
+    $this->assertNull(InlineBlockContent::load($id));
+  }
+
+  /**
+   * Asserts that a value is numeric.
+   *
+   * @param mixed $value
+   *   The value to check.
+   */
+  protected function assertNumeric($value) {
+    $this->assertTrue(is_numeric($value));
+  }
+
+  /**
+   * Asserts that a shadow block and standard custom block are identical.
+   *
+   * @param \Drupal\block_content\BlockContentInterface $original
+   *   The original standard custom block.
+   * @param \Drupal\block_content\BlockContentInterface $shadow
+   *   The shadow block.
+   */
+  protected function assertShadow(BlockContentInterface $original, BlockContentInterface $shadow) {
+    $this->assertInstanceOf(InlineBlockContent::class, $shadow);
+    // Shadow blocks should masquerade as normal custom blocks.
+    $this->assertSame('block_content', $shadow->getEntityTypeId());
+    $this->assertSame($original->isNew(), $shadow->isNew());
+    $this->assertSame($original->id(), $shadow->id());
+    $this->assertSame($original->label(), $shadow->label());
+    $this->assertSame($original->uuid(), $shadow->uuid());
+
+    // Shadow blocks should have all custom block fields and load their values.
+    $this->assertTrue($shadow->hasField('body'));
+    $this->assertSame($original->body->value, $shadow->body->value);
+  }
+
+}
diff --git a/src/SerializableTempstore.php b/src/SerializableTempstore.php
new file mode 100644
index 0000000..3eed41c
--- /dev/null
+++ b/src/SerializableTempstore.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\ctools;
+
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\user\SharedTempStore;
+
+class SerializableTempstore extends SharedTempStore {
+  use DependencySerializationTrait;
+}
diff --git a/src/SerializableTempstoreFactory.php b/src/SerializableTempstoreFactory.php
new file mode 100644
index 0000000..6a48b2f
--- /dev/null
+++ b/src/SerializableTempstoreFactory.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\ctools;
+
+use Drupal\user\SharedTempStoreFactory;
+
+class SerializableTempstoreFactory extends SharedTempStoreFactory {
+
+  /**
+   * {@inheritdoc}
+   */
+  function get($collection, $owner = NULL) {
+    // Use the currently authenticated user ID or the active user ID unless
+    // the owner is overridden.
+    if (!isset($owner)) {
+      $owner = \Drupal::currentUser()->id() ?: session_id();
+    }
+
+    // Store the data for this collection in the database.
+    $storage = $this->storageFactory->get("user.shared_tempstore.$collection");
+    return new SerializableTempstore($storage, $this->lockBackend, $owner, $this->requestStack, $this->expire);
+  }
+
+}
