diff --git a/config/schema/webform.schema.yml b/config/schema/webform.schema.yml
index f65df91..c5a69ca 100644
--- a/config/schema/webform.schema.yml
+++ b/config/schema/webform.schema.yml
@@ -88,3 +88,15 @@ webform.settings:
     update_batch_size:
       type: integer
       label: 'Update batch size'
+
+# Webform type config schema
+webform.type.*:
+  type: config_entity
+  label: 'Webform Type'
+  mapping:
+    id:
+      type: string
+      label: 'Machine name'
+    name:
+      type: label
+      label: 'Name'
diff --git a/src/Controller/WebformController.php b/src/Controller/WebformController.php
index f20ea51..3e79b4e 100644
--- a/src/Controller/WebformController.php
+++ b/src/Controller/WebformController.php
@@ -10,6 +10,7 @@ namespace Drupal\webform\Controller;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Url;
 use Drupal\node\Entity\Node;
+use Drupal\webform\Entity\WebformTypeInterface;
 
 /**
  * Returns responses for Webform routes.
@@ -74,4 +75,22 @@ class WebformController extends ControllerBase {
     return $table;
   }
 
+  /**
+   * Provides the webform submission form.
+   *
+   * @param \Drupal\webform\Entity\WebformTypeInterface $webform_type
+   *   The webform type to use for the webform.
+   *
+   * @return array
+   *   A webform submission form.
+   */
+  public function add(WebformTypeInterface $webform_type) {
+    $webform = $this->entityManager()->getStorage('webform')->create(array(
+      'type' => $webform_type->id(),
+    ));
+
+    $form = $this->entityFormBuilder()->getForm($webform);
+    return $form;
+  }
+
 }
diff --git a/src/Entity/Webform.php b/src/Entity/Webform.php
new file mode 100644
index 0000000..e21ad0f
--- /dev/null
+++ b/src/Entity/Webform.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Entity\Webform
+ */
+
+namespace Drupal\webform\Entity;
+
+use Drupal\Core\Entity\Entity;
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Provides a webform entity to which configurable fields can be attached.
+ *
+ * @ContentEntityType(
+ *   id = "webform",
+ *   label = @Translation("Webform"),
+ *   bundle_label = @Translation("Webform type"),
+ *   handlers = {
+ *     "storage" = "Drupal\webform\Storage\WebformStorage",
+ *     "storage_schema" = "Drupal\webform\Storage\WebformStorageSchema",
+ *     "form" = {
+ *       "default" = "Drupal\webform\Form\WebformForm",
+ *       "delete" = "Drupal\webform\Form\WebformDeleteForm",
+ *       "edit" = "Drupal\webform\Form\WebformForm"
+ *     },
+ *     "list_builder" = "Drupal\webform\WebformListBuilder",
+ *   },
+ *   field_ui_base_route = "entity.webform_type.edit_form",
+ *   base_table = "webform_temp",
+ *   entity_keys = {
+ *     "id" = "webform_id",
+ *     "bundle" = "type",
+ *     "uuid" = "uuid"
+ *   },
+ *   bundle_entity_type = "webform_type",
+ *   links = {
+ *     "canonical" = "/webform/{webform}",
+ *     "delete-form" = "/webform/{webform}/delete",
+ *     "edit-form" = "/webform/{webform}/edit",
+ *   }
+ * )
+ */
+
+class Webform extends ContentEntityBase implements WebformInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields['webform_id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('ID'))
+      ->setDescription(t('The webform ID.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['uuid'] = BaseFieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The webform UUID.'))
+      ->setReadOnly(TRUE);
+
+    $fields['type'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Type'))
+      ->setDescription(t('The webform type.'))
+      ->setSetting('target_type', 'webform_type')
+      ->setReadOnly(TRUE);
+
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypeLabel() {
+    $type = WebformType::load($this->bundle());
+    return $type ? $type->label() : FALSE;
+  }
+
+}
diff --git a/src/Entity/WebformInterface.php b/src/Entity/WebformInterface.php
new file mode 100644
index 0000000..5981a62
--- /dev/null
+++ b/src/Entity/WebformInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Entity\WebformInterface
+ */
+
+namespace Drupal\webform\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ *
+ * Provides an interface for the webform content entity.
+ */
+interface WebformInterface extends ContentEntityInterface {
+
+  /**
+   * Returns the type label of a webform.
+   *
+   * @return string|FALSE
+   *   The type label if the type exists, or FALSE.
+   */
+  public function getTypeLabel();
+
+}
diff --git a/src/Entity/WebformType.php b/src/Entity/WebformType.php
new file mode 100644
index 0000000..0a4875e
--- /dev/null
+++ b/src/Entity/WebformType.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Entity\WebformType
+ */
+
+namespace Drupal\webform\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+
+/**
+ * Defines a webform type configuration entity.
+ *
+ * @ConfigEntityType(
+ *   id = "webform_type",
+ *   label = @Translation("Webform type"),
+ *   handlers = {
+ *     "form" = {
+ *       "add" = "Drupal\webform\Form\WebformTypeForm",
+ *       "edit" = "Drupal\webform\Form\WebformTypeForm",
+ *       "delete" = "Drupal\webform\Form\WebformTypeDeleteForm"
+ *     },
+ *     "list_builder" = "Drupal\webform\WebformTypeListBuilder",
+ *   },
+ *   admin_permission = "edit webform components",
+ *   config_prefix = "type",
+ *   bundle_of = "webform",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "name"
+ *   },
+ *   links = {
+ *     "edit-form" = "/admin/structure/webform/{webform_type}",
+ *     "delete-form" = "/admin/structure/webform/{webform_type}/delete",
+ *     "collection" = "/admin/structure/webform/types",
+ *   }
+ * )
+ */
+class WebformType extends ConfigEntityBundleBase implements WebformTypeInterface {
+
+  /**
+   * The machine name of the webform type.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The name of the webform type.
+   *
+   * @var string
+   */
+  protected $name;
+
+}
diff --git a/src/Entity/WebformTypeInterface.php b/src/Entity/WebformTypeInterface.php
new file mode 100644
index 0000000..84a179a
--- /dev/null
+++ b/src/Entity/WebformTypeInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Entity\WebformTypeInterface.
+ */
+
+namespace Drupal\webform\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
+
+/**
+ * Provides an interface for a webform type.
+ */
+interface WebformTypeInterface extends ConfigEntityInterface {
+
+}
diff --git a/src/Form/WebformDeleteForm.php b/src/Form/WebformDeleteForm.php
new file mode 100644
index 0000000..d082e53
--- /dev/null
+++ b/src/Form/WebformDeleteForm.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformDeleteForm
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+
+/**
+ * Provides a form for deleting a webform.
+ */
+class WebformDeleteForm extends ContentEntityDeleteForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
+    $this->entity->delete();
+    $this->logger('webform')->notice('Webform submission @id deleted.', array('@id' => $this->entity->id()));
+    drupal_set_message($this->t('Submission deleted.'));
+    $form_state->setRedirect('<front>');
+  }
+}
diff --git a/src/Form/WebformForm.php b/src/Form/WebformForm.php
new file mode 100644
index 0000000..59a1d26
--- /dev/null
+++ b/src/Form/WebformForm.php
@@ -0,0 +1,83 @@
+<?php
+
+/*
+ * @file
+ * Contains \Drupal\webform\Form\WebformForm
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+
+/**
+ * Form controller that provides a webform submission form.
+ */
+class WebformForm extends ContentEntityForm {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $current_user;
+
+  /**
+   * Constructs a WebformForm object.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Session\AccountInterface $user
+   *   The current user.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, AccountInterface $user) {
+    $this->entityManager = $entity_manager;
+    $this->current_user = $user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager'),
+      $container->get('current_user')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
+    $webform = $this->entity;
+    $insert = $webform->isNew();
+    $webform->save();
+
+    if ($webform->id()) {
+      // Tell the submitter what happened, and log the action.
+      if ($insert) {
+        drupal_set_message($this->t('Submission completed.'));
+        $this->logger('webform')->notice('Webform submission @id created.', array('@id' => $webform->id()));
+      }
+      else {
+        drupal_set_message($this->t('Submission updated.'));
+        $this->logger('webform')->notice('Webform submission @id updated.', array('@id' => $webform->id()));
+      }
+
+      // Redirect the submitter.
+      if ($webform->access('view')) {
+        $form_state->setRedirect('entity.webform.canonical', array('webform' => $webform->id()));
+      }
+      else {
+        $form_state->setRedirect('<front>');
+      }
+    }
+    else {
+      drupal_set_message($this->t('The form could not be saved.'));
+    }
+  }
+
+}
diff --git a/src/Form/WebformTypeDeleteForm.php b/src/Form/WebformTypeDeleteForm.php
new file mode 100644
index 0000000..40d1e48
--- /dev/null
+++ b/src/Form/WebformTypeDeleteForm.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformTypeDeleteForm
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Entity\EntityDeleteForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a form handler to delete webform types.
+ */
+class WebformTypeDeleteForm extends EntityDeleteForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $this->entity->delete();
+    drupal_set_message($this->t('Webform type %type deleted.', array('%type' => $this->entity->bundle())));
+    $this->logger('webform')->notice('Webform type @type deleted.', array('@type' => $this->entity->bundle()));
+  }
+
+}
diff --git a/src/Form/WebformTypeForm.php b/src/Form/WebformTypeForm.php
new file mode 100644
index 0000000..47b648f
--- /dev/null
+++ b/src/Form/WebformTypeForm.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformTypeForm
+ */
+
+// @todo Revisit string management
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Component\Utility\String;
+
+/**
+ * Form handler for webform type forms.
+ */
+class WebformTypeForm extends EntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Set the title depending on the operation.
+    $type = $this->entity;
+    if ($this->operation == 'add') {
+      $form['#title'] = $this->t('Add Webform Type');
+    }
+    else {
+      $form['#title'] = $this->t('Edit Webform Type %type', array('%type' => $type->label()));
+    }
+
+    $form['name'] = array(
+      '#title' => t('Name'),
+      '#type' => 'textfield',
+      '#default_value' => $type->label(),
+      '#description' => t('The name of this Webform type.'),
+      '#required' => TRUE,
+      '#size' => 30,
+    );
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $type->id(),
+      '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
+      '#disabled' => !$type->isNew(),
+      '#machine_name' => array(
+        'exists' => 'Drupal\webform\Entity\WebformType::load',
+        'source' => array('name'),
+      ),
+      '#description' => t('A unique machine-readable name for this webform type. It must only contain lowercase letters, numbers, and underscores.'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array $form, FormStateInterface $form_state) {
+    parent::validate($form, $form_state);
+
+    // Make sure the machine name is not empty
+    // @see NodeTypeForm->validate()
+    // @todo Shouldn't this be done automatically by form api?
+    $id = trim($form_state->getValue('id'));
+    if ($id == '0') {
+      $form_state->setErrorByName('id', $this->t("Invalid machine-readable name. Enter a name other than %invalid.", array('%invalid' => $id)));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $type = $this->entity;
+    // @see NodeTypeForm->save()
+    // @todo Again, shouldn't this be done automatically by form api?
+    $type->set('id', trim($type->id()));
+    $type->set('name', trim($type->label()));
+
+    $status = $type->save();
+    if ($status == SAVED_UPDATED) {
+      drupal_set_message($this->t('Webform type %type was updated.', array('%type' => $type->label())));
+      $this->logger('webform')->notice('Webform type @type was updated.', array('@type' => $type->label()));
+    }
+    elseif ($status == SAVED_NEW) {
+      drupal_set_message($this->t('Webform type %type was created.', array('%type' => $type->label())));
+      $this->logger('webform')->notice('Webform type @type was created.', array('@type' => $type->label()));
+    }
+
+    $form_state->setRedirectUrl($type->urlInfo('collection'));
+  }
+
+}
diff --git a/src/Storage/WebformStorage.php b/src/Storage/WebformStorage.php
new file mode 100644
index 0000000..a2a427e
--- /dev/null
+++ b/src/Storage/WebformStorage.php
@@ -0,0 +1,415 @@
+<?php
+
+/*
+ * @file
+ * Contains \Drupal\webform\Storage\WebformStorage
+ */
+
+namespace Drupal\webform\Storage;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Entity\Sql\DefaultTableMapping;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Provides an SQL storage handler for the webform content entity whereby all
+ * fields are stored in a single table.
+ *
+ * @todo Review schema design - do we need both field and field_storage?
+ */
+class WebformStorage extends SqlContentEntityStorage {
+
+  /**
+   * The table that stores configurable field data.
+   *
+   * @var string
+   */
+  protected $field_table = 'webform_temp_submissions';
+
+  /**
+   * {@inheritdoc}
+   *
+   * Add the field table to the table map.
+   */
+  public function getTableMapping(array $storage_definitions = NULL) {
+    $table_mapping = $this->tableMapping;
+
+    // First map the base table.
+
+    // If we are using our internal storage definitions, which is our main use
+    // case, we can statically cache the computed table mapping. If a new set
+    // of field storage definitions is passed, for instance when comparing old
+    // and new storage schema, we compute the table mapping without caching.
+    // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
+    //   easily instantiate a new table mapping whenever needed.
+    if (!isset($this->tableMapping) || $storage_definitions) {
+      $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
+      $table_mapping = new DefaultTableMapping($this->entityType, $definitions);
+
+      $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
+        return $table_mapping->allowsSharedTableStorage($definition);
+      });
+
+      $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey)));
+      $all_fields = array_keys($definitions);
+      $revisionable_fields = array_keys(array_filter($definitions, function (FieldStorageDefinitionInterface $definition) {
+        return $definition->isRevisionable();
+      }));
+      // Make sure the key fields come first in the list of fields.
+      $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
+
+      // The base layout stores all the base field values in the base table.
+      $table_mapping->setFieldNames($this->baseTable, $all_fields);
+
+      // Now map the field_table.
+      $table_mapping->setExtraColumns($this->field_table, array(
+        'wid',
+        'vid',
+        'bundle',
+        'field',
+        'field_storage',
+        'delta',
+        'deleted',
+        'langcode',
+        'data',
+      ));
+
+      // Cache the computed table mapping only if we are using our internal
+      // storage definitions.
+      if (!$storage_definitions) {
+        $this->tableMapping = $table_mapping;
+      }
+    }
+
+    return $table_mapping;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Override this method to load from the field table.
+   */
+  protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
+    if (empty($values)) {
+      return;
+    }
+
+    // Fetch ids and bundles.
+    foreach ($values as $key => $entity_values) {
+      $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
+      $ids[] = $key;
+    }
+
+    // Fetch field storage definitions.
+    $storage_definitions = array();
+    $definitions = array();
+    $table_mapping = $this->getTableMapping();
+    foreach ($bundles as $bundle => $v) {
+      $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
+      foreach ($definitions[$bundle] as $field_name => $field_definition) {
+        $storage_definition = $field_definition->getFieldStorageDefinition();
+        if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+          $storage_definitions[$field_name] = $storage_definition;
+        }
+      }
+    }
+
+    // Load field data.
+    $results = $this->database->select($this->field_table, 't')
+      ->fields('t')
+      ->condition('wid', $ids, 'IN')
+      ->condition('deleted', 0)
+      ->execute();
+
+    $langcode = LanguageInterface::LANGCODE_DEFAULT;
+
+    // Populate the fields with retrieved values.
+    //   What if field schema has changed since the record was saved?
+    foreach ($results as $row) {
+      $item = array();
+      // Map the 'data' column to the field schema.
+      $field_columns = $storage_definitions[$row->field_storage]->getColumns();
+      if (count($field_columns) > 1) {
+        $values[$row->wid][$row->field][$langcode][] = unserialize($row->data);
+      }
+      else {
+        reset($field_columns);
+        $field_column = key($field_columns);
+        $values[$row->wid][$row->field][$langcode][] = array($field_column => (!empty($field_columns[$field_column]['serialize'])) ? unserialize($row->data) : $row->data);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Override this method to save to the field table.
+   */
+  protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE) {
+    $id = $entity->id();
+    $bundle = $entity->bundle();
+    $entity_type = $entity->getEntityTypeId();
+    $default_langcode = $entity->getUntranslated()->language()->getId();
+    $table_mapping = $this->getTableMapping();
+
+    if (!isset($vid)) {
+      $vid = $id;
+    }
+
+    // We can do a single multi-insert across all fields.  Prepare the insert.
+    $do_insert = FALSE;
+    $insert_query = $this->database->insert($this->field_table)
+      ->fields($table_mapping->getAllColumns($this->field_table));
+    foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) {
+      $storage_definition = $field_definition->getFieldStorageDefinition();
+      if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+        continue;
+      }
+
+      // Each field may have multiple sets of values.  Add each.
+      $items = $entity->getTranslation($default_langcode)->get($field_name);
+      $items->filterEmptyItems();
+      $delta_count = 0;
+      foreach ($items as $delta => $item) {
+        // We now know we have something to insert.
+        $do_insert = TRUE;
+        $record = array(
+          'wid' => $id,
+          'vid' => $vid,
+          'bundle' => $bundle,
+          'field' => $field_definition->getName(),
+          'field_storage' => $storage_definition->getName(),
+          'delta' => $delta,
+          'deleted' => 0,
+          'langcode' => $default_langcode,
+        );
+
+        // Map the field schema to the 'data' column.
+        $field_columns = $storage_definition->getColumns();
+        // If more than one field column, serialization is needed.
+        if (count($field_columns) > 1) {
+          $values = array();
+          foreach ($field_columns as $column => $attributes) {
+            $values[$column] = $item->$column;
+          }
+
+          $record['data'] = serialize($values);
+        }
+        else {
+          foreach ($field_columns as $column => $attributes) {
+            // A single field_column may require serialization if specified.
+            $record['data'] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
+          }
+        }
+
+        $insert_query->values($record);
+
+        if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
+          break;
+        }
+      }
+    }
+
+    // If this is an update, delete first and then insert.
+    if ($update) {
+      $this->database->delete($this->field_table)
+        ->condition('wid', $id)
+        ->execute();
+    }
+
+    // Execute the query if we have values to insert.
+    if ($do_insert) {
+      $insert_query->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Override this method to delete from the field table
+   */
+  protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
+    $this->database->delete($this->field_table)
+      ->condition('wid', $entity->id(), '=')
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Remove the FieldStorageConfigInterface check when non-configurable
+    //   fields support purging: https://www.drupal.org/node/2282119.
+    if ($storage_definition instanceof FieldStorageConfigInterface) {
+      $this->database->update($this->field_table)
+        ->fields(array('deleted' => 1))
+        ->condition('field_storage', $storage_definition->getName(), '=')
+        ->execute();
+    }
+
+    // Update the field schema.
+    $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
+    $this->database->update($this->field_table)
+      ->fields(array('deleted' => 1))
+      ->condition('field', $field_definition->getName(), '=')
+      // @todo If we could do an inner join here on the base table, we could
+      // eliminate the bundle key in the field table.  Update query class
+      // doesn't provide means.
+      // Alternatively we could create a table for each bundle.
+      ->condition('bundle', $field_definition->getTargetBundle())
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onBundleRename($bundle, $bundle_new, $entity_type_id) {
+    if ($this->entityTypeId == $entity_type_id) {
+      $this->database->update($this->field_table)
+      ->fields(array('bundle' => $bundle_new))
+      ->condition('bundle', $bundle, '=')
+      ->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   * // @todo
+   */
+  protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
+    // Check whether the whole field storage definition is gone, or just some
+    // bundle fields.
+    $storage_definition = $field_definition->getFieldStorageDefinition();
+    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+
+    // Get the entities which we want to purge first.
+    $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC));
+    $or = $entity_query->orConditionGroup();
+    foreach ($storage_definition->getColumns() as $column_name => $data) {
+      $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
+    }
+    $entity_query
+      ->distinct(TRUE)
+      ->fields('t', array('entity_id'))
+      ->condition('bundle', $field_definition->getTargetBundle())
+      ->range(0, $batch_size);
+
+    // Create a map of field data table column names to field column names.
+    $column_map = array();
+    foreach ($storage_definition->getColumns() as $column_name => $data) {
+      $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
+    }
+
+    $entities = array();
+    $items_by_entity = array();
+    foreach ($entity_query->execute() as $row) {
+      $item_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC))
+        ->fields('t')
+        ->condition('entity_id', $row['entity_id'])
+        ->orderBy('delta');
+
+      foreach ($item_query->execute() as $item_row) {
+        if (!isset($entities[$item_row['revision_id']])) {
+          // Create entity with the right revision id and entity id combination.
+          $item_row['entity_type'] = $this->entityTypeId;
+          // @todo: Replace this by an entity object created via an entity
+          // factory, see https://drupal.org/node/1867228.
+          $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
+        }
+        $item = array();
+        foreach ($column_map as $db_column => $field_column) {
+          $item[$field_column] = $item_row[$db_column];
+        }
+        $items_by_entity[$item_row['revision_id']][] = $item;
+      }
+    }
+
+    // Create field item objects and return.
+    foreach ($items_by_entity as $revision_id => $values) {
+      $entity_adapter = $entities[$revision_id]->getTypedData();
+      $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
+    }
+    return $items_by_entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
+    $this->database->delete($this->field_table)
+      ->condition('wid', $entity->id())
+      ->condition('field', $field_definition->getName(), '=')
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countFieldData($storage_definition, $as_bool = FALSE) {
+    $table_mapping = $this->getTableMapping();
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      $query = $this->database->select($this->field_table, 't')
+        ->fields('t', array('wid', 'field'))
+        ->condition('field_storage', $storage_definition->getName(), '=')
+        ->distinct(TRUE);
+      // @todo check for null values as in parent method?
+    }
+    // This block copied from parent method.
+    elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
+      // Ascertain the table this field is mapped too.
+      $field_name = $storage_definition->getName();
+      try {
+        $table_name = $table_mapping->getFieldTableName($field_name);
+      }
+      catch (SqlContentEntityStorageException $e) {
+        // This may happen when changing field storage schema, since we are not
+        // able to use a table mapping matching the passed storage definition.
+        // @todo Revisit this once we are able to instantiate the table mapping
+        //   properly. See https://www.drupal.org/node/2274017.
+        $table_name = $this->dataTable ?: $this->baseTable;
+      }
+      $query = $this->database->select($table_name, 't');
+      $or = $query->orConditionGroup();
+      foreach (array_keys($storage_definition->getColumns()) as $property_name) {
+        $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
+      }
+      $query->condition($or);
+      $query
+        ->fields('t', array($this->idKey))
+        ->distinct(TRUE);
+    }
+
+    // @todo Find a way to count field data also for fields having custom
+    //   storage. See https://www.drupal.org/node/2337753.
+    $count = 0;
+    if (isset($query)) {
+      // If we are performing the query just to check if the field has data
+      // limit the number of rows.
+      if ($as_bool) {
+        $query->range(0, 1);
+      }
+      else {
+        // Otherwise count the number of rows.
+        $query = $query->countQuery();
+      }
+      $count = $query->execute()->fetchField();
+    }
+    return $as_bool ? (bool) $count : (int) $count;
+  }
+
+}
diff --git a/src/Storage/WebformStorageSchema.php b/src/Storage/WebformStorageSchema.php
new file mode 100644
index 0000000..331e1bc
--- /dev/null
+++ b/src/Storage/WebformStorageSchema.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Storage\WebformStorageSchema
+ */
+
+namespace Drupal\webform\Storage;
+
+use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\FieldStorageConfigInterface;
+
+/**
+ * Provides a storage schema handler for the webform content type.
+ */
+class WebformStorageSchema extends SqlContentEntityStorageSchema {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Parent method will rename dedicated tables; since those don't exist,
+   * we need to override.
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // Only configurable fields currently support purging, so prevent deletion
+    // of ones we can't purge if they have existing data.
+    // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
+    try {
+      if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
+        throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that cannot be purged.');
+      }
+    }
+    catch (DatabaseException $e) {
+      // This may happen when changing field storage schema, since we are not
+      // able to use a table mapping matching the passed storage definition.
+      // @todo Revisit this once we are able to instantiate the table mapping
+      //   properly. See https://www.drupal.org/node/2274017.
+      return;
+    }
+
+    // @todo Remove when finalizePurge() is invoked from the outside for all
+    //   fields: https://www.drupal.org/node/2282119.
+    if (!($storage_definition instanceof FieldStorageConfigInterface)) {
+      $this->performFieldSchemaOperation('delete', $storage_definition);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Parent method compares new field schema with old loaded from key-value
+   * store.
+   *
+   * @todo Can we in fact bypass the key-value store for field schema
+   *   storage entirely, or is it needed to identify field schema changes?
+   *   Is the comparison '$storage_definition->getSchema() != $original->getSchema()' sufficient?
+   */
+  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $table_mapping = $this->storage->getTableMapping();
+
+    if (
+      $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
+      $storage_definition->getSchema() != $original->getSchema() ||
+      $storage_definition->isRevisionable() != $original->isRevisionable() ||
+      $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
+      $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
+    ) {
+      return TRUE;
+    }
+
+    if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
+      $field_name = $storage_definition->getName();
+      $schema = array();
+      foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
+        if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
+          $column_names = $table_mapping->getColumnNames($storage_definition->getName());
+          $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
+        }
+      }
+      return $schema != $this->loadFieldSchemaData($original);
+    }
+    else {
+      // The field has custom storage, so we don't know if a schema change is
+      // needed or not, but since per the initial checks earlier in this
+      // function, nothing about the definition changed that we manage, we
+      // return FALSE.
+      return FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Do nothing.
+   */
+  protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Do nothing.
+   */
+  protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Do nothing.
+   */
+  protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { }
+
+  /**
+   * {@inheritdoc}
+   * @todo Review in context of requiresFieldStorageSchemaChanges()
+   *
+   * @see hook_schema()
+   */
+  /*
+  protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
+
+  }
+   *
+   */
+
+}
diff --git a/src/WebformListBuilder.php b/src/WebformListBuilder.php
new file mode 100644
index 0000000..9238647
--- /dev/null
+++ b/src/WebformListBuilder.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\WebformListBuilder
+ */
+
+namespace Drupal\webform;
+
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Class handler to create a list of webform entities.
+ */
+class WebformListBuilder extends EntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['type']['data'] = $this->t('Webform Type');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $uri = $entity->urlInfo();
+    $row['type']['data'] = array(
+      '#type' => 'link',
+      '#title' => $entity->getTypeLabel(),
+      '#url' => $uri,
+    );
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No Webform submissions have yet been made.');
+    return $build;
+  }
+
+}
diff --git a/src/WebformTypeListBuilder.php b/src/WebformTypeListBuilder.php
new file mode 100644
index 0000000..efc26e7
--- /dev/null
+++ b/src/WebformTypeListBuilder.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\WebformTypeListBuilder
+ */
+
+namespace Drupal\webform;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Component\Utility\Xss;
+
+/**
+ * A handler for listing webform types.
+ */
+class WebformTypeListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['name'] = $this->t('Name');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row['name'] = $this->getLabel($entity);
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No webform types have been created yet.');
+    return $build;
+  }
+
+}
\ No newline at end of file
diff --git a/webform.install b/webform.install
index c439c43..c571889 100644
--- a/webform.install
+++ b/webform.install
@@ -664,6 +664,73 @@ function webform_schema() {
     'primary key' => array('nid', 'uid'),
   );
 
+  $schema['webform_temp_submissions'] = array(
+    'description' => 'Stores submission field data.',
+    'fields' => array(
+      'wid' => array(
+        'description' => 'The webform id.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'vid' => array(
+        'description' => 'The webform version id.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      // @todo - Check valid bundle machine_name length
+      'bundle' => array(
+        'description' => 'The webform bundle.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      // @todo - Check valid field machine_name length
+      'field' => array(
+        'description' => 'The field configuration machine name.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'field_storage' => array(
+        'description' => 'The field storage configuration machine name.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'delta' => array(
+        'description' => 'The field delta.',
+        'type' => 'int',
+        'not null' => TRUE,
+      ),
+      'deleted' => array(
+        'description' => 'Whether the field has been deleted.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'langcode' => array(
+        'description' => 'The language code.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      // @todo Is this big enough to accomodate all field types?  Do we need a check somewhere?
+      'data' => array(
+        'description' => 'The value(s) of the field delta.  Serialized if more than one value.',
+        'type' => 'text',
+        'size' => 'medium',
+        'not null' => TRUE,
+      ),
+    ),
+    'primary key' => array('wid', 'vid', 'deleted', 'field', 'bundle', 'delta', 'langcode'),
+    'indexes' => array(
+      'field_storage' => array('field_storage'),
+      'wid' => array('wid'),
+    ),
+  );
+
   return $schema;
 }
 
diff --git a/webform.links.action.yml b/webform.links.action.yml
new file mode 100644
index 0000000..081709b
--- /dev/null
+++ b/webform.links.action.yml
@@ -0,0 +1,11 @@
+webform.type.add:
+  route_name: webform.type.add
+  title: 'Add Webform type'
+  appears_on:
+    - entity.webform_type.collection
+    
+webform.type.view_submissions:
+  route_name: entity.webform.collection
+  title: 'View submissions'
+  appears_on:
+    - entity.webform_type.collection
diff --git a/webform.links.menu.yml b/webform.links.menu.yml
index fee7ac3..b04a54b 100644
--- a/webform.links.menu.yml
+++ b/webform.links.menu.yml
@@ -3,3 +3,16 @@ webform.settings:
   description: 'Global configuration of webform functionality.'
   route_name: webform.settings
   parent: system.admin_config_content
+  
+entity.webform_type.collection:
+  title: 'Webform types'
+  parent: system.admin_structure
+  description: 'Manage webform types.'
+  route_name: entity.webform_type.collection
+  
+entity.webform.collection:
+  title: 'Submissions'
+  parent: system.admin
+  description: 'Review/manage webform submissions.'
+  route_name: entity.webform.collection
+  weight: -9
diff --git a/webform.links.task.yml b/webform.links.task.yml
index d44dc74..1beeed7 100644
--- a/webform.links.task.yml
+++ b/webform.links.task.yml
@@ -2,3 +2,24 @@ webform.content_overview:
   title: 'Webforms'
   route_name: webform.content_overview
   base_route: system.admin_content
+
+# Individual submission links
+entity.webform.canonical:
+  route_name: entity.webform.canonical
+  base_route: entity.webform.canonical
+  title: 'View'
+entity.webform.edit_form:
+  route_name: entity.webform.edit_form
+  base_route: entity.webform.canonical
+  title: Edit
+entity.webform.delete_form:
+  route_name: entity.webform.delete_form
+  base_route: entity.webform.canonical
+  title: Delete
+  weight: 10
+
+# Webform type  
+entity.webform_type.edit_form:
+  title: 'Edit'
+  route_name: entity.webform_type.edit_form
+  base_route: entity.webform_type.edit_form
\ No newline at end of file
diff --git a/webform.module b/webform.module
index 5a620eb..fea0ce5 100644
--- a/webform.module
+++ b/webform.module
@@ -697,11 +697,14 @@ function webform_theme() {
     'webform_view_messages' => array(
       'variables' => array('node' => NULL, 'page' => NULL, 'submission_count' => NULL, 'user_limit_exceeded' => NULL, 'total_limit_exceeded' => NULL, 'allowed_roles' => NULL, 'closed' => NULL, 'cached' => NULL),
     ),
+    /**
     'webform_form' => array(
       'render element' => 'form',
       'template' => 'templates/webform-form',
       'pattern' => 'webform_form_[0-9]+',
     ),
+     * 
+     */
     'webform_confirmation' => array(
       'variables' => array('node' => NULL, 'sid' => NULL),
       'template' => 'templates/webform-confirmation',
diff --git a/webform.routing.yml b/webform.routing.yml
index fa50a9a..fc15a74 100644
--- a/webform.routing.yml
+++ b/webform.routing.yml
@@ -12,3 +12,82 @@ webform.settings:
     _form: '\Drupal\webform\Form\WebformSettingsForm'
   requirements:
     _permission: 'administer site configuration'
+
+    
+##### Webform Types #####     
+webform.type.add:
+  path: '/admin/structure/webform/add'
+  defaults:
+    _title: 'Add Webform Type'
+    _entity_form: 'webform_type.add'
+  requirements:
+    _permission: 'edit webform components'
+      
+entity.webform_type.edit_form:
+  path: '/admin/structure/webform/{webform_type}'
+  defaults:
+    _title: 'Edit Webform Type'
+    _entity_form: 'webform_type.edit' 
+  requirements:
+    _permission: 'edit webform components'
+        
+entity.webform_type.delete_form:
+  path: '/admin/structure/webform/{webform_type}/delete'
+  defaults:
+    _title: 'Delete Webform Type'
+    _entity_form: 'webform_type.delete'
+  requirements:
+    _permission: 'edit webform components'
+
+entity.webform_type.collection:
+  path: '/admin/structure/webform/types'
+  defaults:
+    _title: 'Webform Types'
+    _controller: '\Drupal\Core\Entity\Controller\EntityListController::listing'
+    entity_type: 'webform_type'
+  requirements:
+    _permission: 'edit webform components'
+
+
+##### Webforms #####
+entity.webform.canonical:
+  path: '/webform/{webform}'
+  defaults:
+    _title: 'View submission'
+    _entity_view: 'webform'  
+  requirements:
+    _permission: 'edit webform components'
+    
+webform.add:
+  path: '/webform/add/{webform_type}'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformController::add'
+    _title: 'Make submission'
+    _entity_form: 'webform.default'
+  requirements:
+    _permission: 'edit webform components'
+    
+entity.webform.edit_form:
+  path: '/webform/{webform}/edit'
+  defaults:
+    _title: 'Edit submission'
+    _entity_form: 'webform.edit' 
+  requirements:
+    _permission: 'edit webform components'
+    
+entity.webform.delete_form:
+  path: '/webform/{webform}/delete'
+  defaults:
+    _title: 'Delete submission'
+    _entity_form: 'webform.delete'
+  requirements:
+    _permission: 'edit webform components'
+  
+entity.webform.collection:
+  path: '/admin/webforms'
+  defaults:
+    _title: 'Webform submissions'
+    _controller: 'Drupal\Core\Entity\Controller\EntityListController::listing'
+    entity_type: 'webform'
+  requirements:
+    _permission: 'edit webform components'
