diff --git a/core/composer.json b/core/composer.json
index bda527f99d..49e1d1e4ed 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -150,7 +150,8 @@
         "drupal/user": "self.version",
         "drupal/views": "self.version",
         "drupal/views_ui": "self.version",
-        "drupal/workflows": "self.version"
+        "drupal/workflows": "self.version",
+        "drupal/workspace": "self.version"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php
index 459be7a24f..980fb8e68f 100644
--- a/core/modules/config/src/Tests/ConfigImportAllTest.php
+++ b/core/modules/config/src/Tests/ConfigImportAllTest.php
@@ -7,6 +7,7 @@
 use Drupal\system\Tests\Module\ModuleTestBase;
 use Drupal\shortcut\Entity\Shortcut;
 use Drupal\taxonomy\Entity\Term;
+use Drupal\workspace\Entity\Workspace;
 
 /**
  * Tests the largest configuration import possible with all available modules.
@@ -92,6 +93,10 @@ public function testInstallUninstall() {
     $shortcuts = Shortcut::loadMultiple();
     entity_delete_multiple('shortcut', array_keys($shortcuts));
 
+    // Delete any workspaces so the workspace module can be uninstalled.
+    $workspaces = Workspace::loadMultiple();
+    \Drupal::entityTypeManager()->getStorage('workspace')->delete($workspaces);
+
     system_list_reset();
     $all_modules = system_rebuild_module_data();
 
diff --git a/core/modules/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module
index d569de2503..442945e0bb 100644
--- a/core/modules/quickedit/quickedit.module
+++ b/core/modules/quickedit/quickedit.module
@@ -193,5 +193,6 @@ function _quickedit_entity_is_latest_revision(ContentEntityInterface $entity) {
     ->sort($entity_definition->getKey('revision'), 'DESC')
     ->range(0, 1)
     ->execute();
-  return $entity->getLoadedRevisionId() == array_keys($revision_ids)[0];
+  $array_keys = array_keys($revision_ids);
+  return $entity->getLoadedRevisionId() == reset($array_keys);
 }
diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php
index f4db81e18e..c86deaeb6e 100644
--- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php
+++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\workspace\Entity\Workspace;
 
 /**
  * Install/uninstall core module and confirm table creation/deletion.
@@ -147,6 +148,12 @@ public function testInstallUninstall() {
         $this->preUninstallForum();
       }
 
+      // Delete all workspaces before uninstall.
+      if ($name == 'workspace') {
+        $workspaces = Workspace::loadMultiple();
+        \Drupal::entityTypeManager()->getStorage('workspace')->delete($workspaces);
+      }
+
       $now_installed_list = \Drupal::moduleHandler()->getModuleList();
       $added_modules = array_diff(array_keys($now_installed_list), array_keys($was_installed_list));
       while ($added_modules) {
diff --git a/core/modules/workspace/css/workspace.admin.css b/core/modules/workspace/css/workspace.admin.css
new file mode 100644
index 0000000000..31b757d7ba
--- /dev/null
+++ b/core/modules/workspace/css/workspace.admin.css
@@ -0,0 +1,91 @@
+.item-list ul.workspace,
+.item-list ul.workspace ul {
+    padding-top: 20px;
+    position: relative;
+    margin: 0;
+}
+
+.item-list .workspace li {
+    float: left;
+    text-align: center;
+    list-style-type: none;
+    position: relative;
+    padding: 20px 5px 0;
+    margin: 0;
+}
+
+/* We will use ::before and ::after to draw the connectors */
+.item-list .workspace li::before,
+.item-list .workspace li::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    right: 50%;
+    border-top: 1px solid #ccc;
+    width: 50%;
+    height: 20px;
+}
+.item-list .workspace li::after {
+    right: auto;
+    left: 50%;
+    border-left: 1px solid #ccc;
+}
+
+/* We need to remove left-right connectors from elements without any siblings */
+.item-list .workspace li:only-child::after,
+.item-list .workspace li:only-child::before {
+    display: none;
+}
+
+/* Remove space from the top of single children */
+.item-list .workspace li:only-child{
+    padding-top: 0;
+}
+
+/* Remove left connector from first child and right connector from last child*/
+.item-list .workspace li:first-child::before,
+.item-list .workspace li:last-child::after {
+    border: 0 none;
+}
+
+/* Adding back the vertical connector to the last nodes */
+.item-list .workspace li:last-child::before {
+    border-right: 1px solid #ccc;
+}
+
+/* Downward connector from parents */
+.item-list ul.workspace ul::before,
+.item-list ul.workspace ul ul::before {
+    content: '';
+    position: absolute; top: 0; left: 50%;
+    border-left: 1px solid #ccc;
+    width: 0;
+    height: 20px;
+}
+
+.item-list .workspace .rev,
+.item-list .workspace .rev__title {
+    display: inline-block;
+    margin: 0;
+}
+
+.item-list .workspace .panel__title {
+    padding: 0;
+}
+
+.item-list .workspace hr {
+    margin: 5px 0;
+}
+
+.rev {
+    margin: 0px 0px 20px;
+    padding: 9px;
+    background: #F8F8F8;
+    border: 1px solid #CCC
+}
+
+.rev__title {
+    font-size: 1em;
+    text-transform: uppercase;
+    margin: 0px;
+}
\ No newline at end of file
diff --git a/core/modules/workspace/css/workspace.switcher.css b/core/modules/workspace/css/workspace.switcher.css
new file mode 100644
index 0000000000..a833a6bccb
--- /dev/null
+++ b/core/modules/workspace/css/workspace.switcher.css
@@ -0,0 +1,26 @@
+/**
+ * @file
+ * Styling for switcher block.
+ */
+
+#block-workspaceswitcher input[type="submit"] {
+  background: transparent;
+  border: 0;
+  border-radius: 0;
+  padding: 0.2em 0 0 0 ;
+  margin: 0;
+  color: #0071b3;
+  border-bottom: 1px dotted;
+  text-decoration: none;
+  font-family: Georgia, "Times New Roman", Times, serif;
+  font-size: 1em;
+}
+
+#block-workspaceswitcher input[type="submit"]:hover {
+  border-bottom-style: solid;
+  color: #018fe2;
+}
+
+#block-workspaceswitcher input[type="submit"].is-active {
+  color: #000000;
+}
diff --git a/core/modules/workspace/css/workspace.toolbar.css b/core/modules/workspace/css/workspace.toolbar.css
new file mode 100644
index 0000000000..03636ced67
--- /dev/null
+++ b/core/modules/workspace/css/workspace.toolbar.css
@@ -0,0 +1,80 @@
+/**
+ * @file
+ * Styling for Workspace toolbar.
+ */
+
+.toolbar .toolbar-icon-workspace:before {
+  background-image: url("../icons/bebebe/workspace.svg");
+}
+
+.toolbar .toolbar-icon-workspace.is-active:before {
+  background-image: url("../icons/ffffff/workspace.svg");
+}
+
+.toolbar .toolbar-icon-workspace-update:before {
+  background-image: url("../icons/bebebe/update.svg");
+}
+
+.toolbar .toolbar-icon-workspace-update.is-active:before {
+  background-image: url("../icons/ffffff/update.svg");
+}
+
+.toolbar .toolbar-bar .workspace-toolbar-tab.toolbar-tab,
+.toolbar .toolbar-bar .workspace-update-toolbar-tab.toolbar-tab,
+.toolbar .toolbar-bar .workspace-deploy-toolbar-tab.toolbar-tab {
+  float: right;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray input[type="submit"],
+.workspace-deploy-toolbar-tab input[type="submit"] {
+  display: block;
+  background: transparent;
+  border: 0;
+  border-radius: 0;
+  margin: 0;
+  padding: 1em 1.3333em;
+  font-weight: bold;
+  font-size: 1em;
+  line-height: 1;
+  color: #DDDDDD;
+}
+
+.workspace-deploy-toolbar-tab input[type="submit"]:hover,
+.workspace-deploy-toolbar-tab input[type="submit"]:focus {
+  background-image: linear-gradient(rgba(255,255,255,0.125) 20%,transparent 200%);
+  background-color: transparent;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray input[type="submit"] {
+  color: #565656;
+  font-weight: normal;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray input[type="submit"]:hover {
+  text-decoration: underline;
+  color: #000000;
+  box-shadow: none;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray input[type="submit"].is-active {
+  color: #000000;
+  font-weight: bold;
+  text-decoration: underline;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray.toolbar-tray-horizontal input[type="submit"] {
+  float: left;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray.toolbar-tray-vertical input[type="submit"] {
+  padding-left: 2.75em;
+  padding-right: 4em;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray.toolbar-tray-vertical form {
+  background-color: #ffffff;
+}
+
+.toolbar #toolbar-item-workspace-switcher-tray a.add-workspace {
+  float: right;
+}
diff --git a/core/modules/workspace/css/workspace.toolbox.css b/core/modules/workspace/css/workspace.toolbox.css
new file mode 100644
index 0000000000..8012b466cd
--- /dev/null
+++ b/core/modules/workspace/css/workspace.toolbox.css
@@ -0,0 +1,152 @@
+html {
+    background-color: #333333;
+}
+
+body,
+.toolbox,
+body.transform-active nav.toolbar-bar,
+body.transform-active div.toolbar-tray {
+    -webkit-transition: all 0.5s ease;
+    -moz-transition: all 0.5s ease;
+    -o-transition: all 0.5s ease;
+    -ms-transition: all 0.5s ease;
+    transition: all 0.5s ease;
+}
+
+.transform-active {
+    margin: 250px 50px 0
+}
+
+.toolbox {
+    position: fixed;
+    height: 200px;
+    width: 100%;
+    top: -250px;
+    left: 0;
+    color: #EEEEFF;
+    overflow: hidden;
+    z-index: 1000;
+    background: #111111;
+    border-bottom: 50px solid #333;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    line-height: 1.5;
+    word-wrap: break-word;
+}
+
+.toolbox-active {
+    top: 0;
+}
+
+.toolbox ul {
+    margin: 0 0 0 50px;
+    padding: 0;
+    position: absolute;
+    bottom: 0;
+}
+
+.toolbox li {
+    list-style: none;
+    float: left;
+}
+
+.toolbox li .button {
+    color: #FFFFFF;
+    border: 0;
+    text-shadow: none;
+    border-radius: 0;
+    padding: 2em 2em 2em 54px;
+    min-width: 200px;
+    text-align: left;
+    margin: 0 3px 0 0;
+    background: #222222 url(../icons/ffffff/ws_icon.svg) no-repeat 20px;
+    background-size: 24px 24px;
+}
+
+.toolbox li .button:hover,
+.toolbox li .button:focus {
+    background: #111111 url(../icons/ffffff/ws_icon.svg) no-repeat 20px;
+    background-size: 24px 24px;
+}
+
+.toolbox div.control-block {
+    width: 33%;
+    position: absolute;
+    right: 0;
+    top: 0;
+    height: 200px;
+    background-color: #333333;
+    padding: 20px 50px;
+    font-weight: bold;
+}
+
+.toolbox div.control-block a {
+    position: relative;
+    top: -2em;
+    right: -4.8em;
+}
+
+.toolbox h3.active-workspace {
+    font-size: 1.7em;
+    margin: 0.5em 0 1em;
+}
+
+.toolbox h3.active-workspace:before {
+    content: "";
+    display: block;
+    background: url(../icons/ffffff/ws_icon.svg) no-repeat;
+    background-size: 48px 48px;
+    width: 48px;
+    height: 48px;
+    float: left;
+    margin: 7px 20px 0 0;
+}
+
+.toolbox button.toolbox-close {
+    position: absolute;
+    top: 10px;
+    right: 5px;
+    background: url(/core/themes/stable/images/core/icons/ffffff/twistie-down.svg) no-repeat center center;
+    background-size: 32px 32px;
+    height: 32px;
+    width: 32px;
+    text-indent: -9999px;
+    border: 0
+}
+
+.toolbox .button {
+    box-sizing: border-box;
+    display: inline-block;
+    position: relative;
+    text-align: center;
+    line-height: normal;
+    cursor: pointer;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    padding: 4px 1.5em;
+    border: 1px solid #a6a6a6;
+    border-radius: 20em;
+    background-color: #f2f1eb;
+    background-image: -webkit-linear-gradient(top, #f6f6f3, #e7e7df);
+    background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df);
+    color: #333;
+    text-decoration: none;
+    text-shadow: 0 1px hsla(0, 0%, 100%, 0.6);
+    font-weight: 600;
+    font-size: 14px;
+    font-size: 0.875rem;
+    -webkit-transition: all 0.1s;
+    transition: all 0.1s;
+    -webkit-font-smoothing: antialiased;
+    margin: 0 1em 0 0;
+}
+
+.toolbox .button.primary {
+    border-color: #1e5c90;
+    background-color: #0071b8;
+    background-image: -webkit-linear-gradient(top, #007bc6, #0071b8);
+    background-image: linear-gradient(to bottom, #007bc6, #0071b8);
+    color: #fff;
+    text-shadow: 0 1px hsla(0, 0%, 0%, 0.5);
+    font-weight: 700;
+    -webkit-font-smoothing: antialiased;
+}
\ No newline at end of file
diff --git a/core/modules/workspace/icons/bebebe/update.svg b/core/modules/workspace/icons/bebebe/update.svg
new file mode 100644
index 0000000000..c114dea91b
--- /dev/null
+++ b/core/modules/workspace/icons/bebebe/update.svg
@@ -0,0 +1 @@
+<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 32 32" height="32px" id="Layer_1" version="1.1" viewBox="0 0 32 32" width="32px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M25.444,4.291c0,0-1.325,1.293-2.243,2.201C18.514,3.068,11.909,3.456,7.676,7.689   c-2.47,2.47-3.623,5.747-3.484,8.983h4C8.051,14.46,8.81,12.205,10.5,10.514c2.663-2.663,6.735-3.043,9.812-1.162   c-1.042,1.032-2.245,2.238-2.245,2.238c-0.841,1.009,0.104,1.592,0.584,1.577l5.624-0.001c0.297,0,0.539,0.001,0.539,0.001   s0.245,0,0.543,0h1.092c0.298,0,0.54-0.243,0.54-0.541V4.895C27.023,4.188,26.247,3.502,25.444,4.291z" fill="#bebebe"/><path d="M6.555,27.709c0,0,1.326-1.293,2.243-2.201c4.688,3.424,11.292,3.036,15.526-1.197   c2.47-2.471,3.622-5.747,3.484-8.983h-4.001c0.142,2.211-0.617,4.467-2.308,6.159c-2.663,2.662-6.735,3.043-9.812,1.161   c1.042-1.032,2.245-2.238,2.245-2.238c0.841-1.01-0.104-1.592-0.584-1.577l-5.624,0.002c-0.297,0-0.54-0.002-0.54-0.002   s-0.245,0-0.543,0H5.551c-0.298,0-0.54,0.242-0.541,0.541v7.732C4.977,27.812,5.753,28.498,6.555,27.709z" fill="#bebebe"/></g></svg>
\ No newline at end of file
diff --git a/core/modules/workspace/icons/bebebe/workspace.svg b/core/modules/workspace/icons/bebebe/workspace.svg
new file mode 100644
index 0000000000..0d893f3d28
--- /dev/null
+++ b/core/modules/workspace/icons/bebebe/workspace.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch -->
+    <title>Combined Shape</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Group-13" transform="translate(-28.000000, -12.000000)" fill="#BEBEBE">
+            <path d="M42,24 L44,24 L44,12 L32,12 L32,14 L42,14 L42,24 Z M28,16 L40,16 L40,28 L28,28 L28,16 Z" id="Combined-Shape"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/core/modules/workspace/icons/ffffff/update.svg b/core/modules/workspace/icons/ffffff/update.svg
new file mode 100644
index 0000000000..61bba5808e
--- /dev/null
+++ b/core/modules/workspace/icons/ffffff/update.svg
@@ -0,0 +1 @@
+<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 32 32" height="32px" id="Layer_1" version="1.1" viewBox="0 0 32 32" width="32px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M25.444,4.291c0,0-1.325,1.293-2.243,2.201C18.514,3.068,11.909,3.456,7.676,7.689   c-2.47,2.47-3.623,5.747-3.484,8.983h4C8.051,14.46,8.81,12.205,10.5,10.514c2.663-2.663,6.735-3.043,9.812-1.162   c-1.042,1.032-2.245,2.238-2.245,2.238c-0.841,1.009,0.104,1.592,0.584,1.577l5.624-0.001c0.297,0,0.539,0.001,0.539,0.001   s0.245,0,0.543,0h1.092c0.298,0,0.54-0.243,0.54-0.541V4.895C27.023,4.188,26.247,3.502,25.444,4.291z" fill="#ffffff"/><path d="M6.555,27.709c0,0,1.326-1.293,2.243-2.201c4.688,3.424,11.292,3.036,15.526-1.197   c2.47-2.471,3.622-5.747,3.484-8.983h-4.001c0.142,2.211-0.617,4.467-2.308,6.159c-2.663,2.662-6.735,3.043-9.812,1.161   c1.042-1.032,2.245-2.238,2.245-2.238c0.841-1.01-0.104-1.592-0.584-1.577l-5.624,0.002c-0.297,0-0.54-0.002-0.54-0.002   s-0.245,0-0.543,0H5.551c-0.298,0-0.54,0.242-0.541,0.541v7.732C4.977,27.812,5.753,28.498,6.555,27.709z" fill="#ffffff"/></g></svg>
\ No newline at end of file
diff --git a/core/modules/workspace/icons/ffffff/workspace.svg b/core/modules/workspace/icons/ffffff/workspace.svg
new file mode 100644
index 0000000000..584ec8ccae
--- /dev/null
+++ b/core/modules/workspace/icons/ffffff/workspace.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch -->
+    <title>Combined Shape</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Group-13" transform="translate(-28.000000, -12.000000)" fill="#FFFFFF">
+            <path d="M42,24 L44,24 L44,12 L32,12 L32,14 L42,14 L42,24 Z M28,16 L40,16 L40,28 L28,28 L28,16 Z" id="Combined-Shape"></path>
+        </g>
+    </g>
+</svg>
diff --git a/core/modules/workspace/icons/ffffff/ws_icon.svg b/core/modules/workspace/icons/ffffff/ws_icon.svg
new file mode 100644
index 0000000000..5cf1052905
--- /dev/null
+++ b/core/modules/workspace/icons/ffffff/ws_icon.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="40px" height="41px" viewBox="0 0 40 41" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
+    <title>ws_icon</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M38.0008027,0 L2.00134371,0 C0.896653589,0 0,0.896653589 0,2.00134371 L0,38.0029529 C0,39.1054928 0.898803837,40.0021464 1.99919346,40.0021464 L38.0008027,40.0021464 C39.1033426,40.0021464 39.9999961,39.1033426 39.9999961,38.0029529 L39.9999961,2.00134371 C40.0021464,0.896653589 39.1054928,0 38.0008027,0 Z M34.0029533,3.99999961 C35.1054932,3.99999961 36.0021468,4.89880345 36.0021468,5.99919308 C36.0021468,7.10173295 35.1033429,7.99838654 34.0029533,7.99838654 C32.9004134,7.99838654 32.0037598,7.0995827 32.0037598,5.99919308 C32.0016096,4.89880345 32.8982632,3.99999961 34.0029533,3.99999961 Z M26.0024165,3.99999961 C27.1049564,3.99999961 28.00161,4.89880345 28.00161,5.99919308 C28.00161,7.10173295 27.1028061,7.99838654 26.0024165,7.99838654 C24.8998766,7.99838654 24.0032231,7.0995827 24.0032231,5.99919308 C24.0010728,4.89880345 24.8977264,3.99999961 26.0024165,3.99999961 Z M18.0018797,3.99999961 C19.1044196,3.99999961 20.0010732,4.89880345 20.0010732,5.99919308 C20.0010732,7.10173295 19.1022694,7.99838654 18.0018797,7.99838654 C16.8993399,7.99838654 16.0026863,7.0995827 16.0026863,5.99919308 C16.0010736,4.89880345 16.8977272,3.99999961 18.0018797,3.99999961 Z M36.0021468,36.0021468 L3.99999961,36.0021468 L3.99999961,12.0005364 L36.0021468,12.0005364 L36.0021468,36.0021468 Z M8.8047297,32.0016096 L15.1963431,32.0016096 C15.6376816,32.0016096 15.9989233,31.6468186 15.9989233,31.1995669 L15.9989233,16.8052665 C15.9989233,16.363928 15.6441323,16.0026863 15.1963431,16.0026863 L8.79397846,16.0026863 C8.35317753,16.0026863 7.9919358,16.3580148 7.9919358,16.8052665 L7.9919358,31.2081679 C8.00053679,31.6468186 8.35532778,32.0016096 8.8047297,32.0016096 Z M20.8031159,24.0010728 L31.2060177,24.0010728 C31.6468186,24.0010728 32.0080603,23.6462818 32.0080603,23.1990301 L32.0080603,16.7966655 C32.0080603,16.3558646 31.6532694,15.9946228 31.2060177,15.9946228 L20.8031159,15.9946228 C20.3623149,15.9946228 20.0010732,16.3494138 20.0010732,16.7966655 L20.0010732,23.1990301 C20.0010732,23.6462818 20.3558642,24.0010728 20.8031159,24.0010728 Z M20.8031159,32.0016096 L31.2060177,32.0016096 C31.6468186,32.0016096 32.0080603,31.6468186 32.0080603,31.1995669 L32.0080603,28.7972019 C32.0080603,28.356401 31.6532694,27.9951592 31.2060177,27.9951592 L20.8031159,27.9951592 C20.3623149,27.9951592 20.0010732,28.3499502 20.0010732,28.7972019 L20.0010732,31.1995669 C20.0010732,31.6468186 20.3558642,32.0016096 20.8031159,32.0016096 Z" id="Icon" fill="#FFFFFF"></path>
+    </g>
+</svg>
diff --git a/core/modules/workspace/js/workspace.toolbox.js b/core/modules/workspace/js/workspace.toolbox.js
new file mode 100644
index 0000000000..9d608ce8cd
--- /dev/null
+++ b/core/modules/workspace/js/workspace.toolbox.js
@@ -0,0 +1,25 @@
+/**
+ * @file
+ * Drupal's Settings Tray library.
+ */
+
+(function ($, Drupal) {
+    Drupal.behaviors.workspaceToolbox = {
+        attach: function (context, settings) {
+            $(context).find('a.toolbar-icon-workspace').once('workspaceToolbox').click(function (event) {
+                $('body').toggleClass('transform-active').css('padding-top', 0);
+                $('div.toolbox').toggleClass('toolbox-active');
+                event.stopPropagation();
+                event.preventDefault();
+                return false;
+            });
+            $(context).find('button.toolbox-close').once('workspaceToolbox').click(function (event) {
+                $('body').toggleClass('transform-active').css('padding-top', '79px');
+                $('div.toolbox').toggleClass('toolbox-active');
+                event.stopPropagation();
+                event.preventDefault();
+                return false;
+            });
+        }
+    };
+})(jQuery, Drupal);
diff --git a/core/modules/workspace/src/Access/WorkspaceViewCheck.php b/core/modules/workspace/src/Access/WorkspaceViewCheck.php
new file mode 100644
index 0000000000..54f5943681
--- /dev/null
+++ b/core/modules/workspace/src/Access/WorkspaceViewCheck.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\workspace\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Class WorkspaceViewCheck
+ */
+class WorkspaceViewCheck implements AccessInterface {
+
+  /**
+   * Checks that the user should be able to view the specified workspace.
+   *
+   * "View" in practice implies "is allowed to make active".
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace to view.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to check.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(WorkspaceInterface $workspace, AccountInterface $account) {
+    return AccessResult::allowedIf($workspace->access('view', $account))->addCacheableDependency($workspace);
+  }
+
+}
diff --git a/core/modules/workspace/src/Annotation/Upstream.php b/core/modules/workspace/src/Annotation/Upstream.php
new file mode 100644
index 0000000000..f306917eeb
--- /dev/null
+++ b/core/modules/workspace/src/Annotation/Upstream.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\workspace\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Class Upstream
+ *
+ * @Annotation
+ */
+class Upstream extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+}
diff --git a/core/modules/workspace/src/Changes/Changes.php b/core/modules/workspace/src/Changes/Changes.php
new file mode 100644
index 0000000000..6ce78537b8
--- /dev/null
+++ b/core/modules/workspace/src/Changes/Changes.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace Drupal\workspace\Changes;
+
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Drupal\workspace\Index\SequenceIndexInterface;
+
+/**
+ * {@inheritdoc}
+ */
+class Changes implements ChangesInterface {
+  use DependencySerializationTrait;
+
+  /**
+   * The workspace to generate the changeset from.
+   *
+   * @var string
+   */
+  protected $workspaceId;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\workspace\Index\SequenceIndex
+   */
+  protected $sequenceIndex;
+
+  /**
+   * Whether to include entities in the changeset.
+   *
+   * @var boolean
+   */
+  protected $includeEntities = FALSE;
+
+  /**
+   * The sequence ID to start including changes from. Result includes $lastSeq.
+   *
+   * @var int
+   */
+  protected $lastSeq = 0;
+
+  /**
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\Workspace\Index\SequenceIndexInterface $sequence_index
+   */
+  public function __construct(WorkspaceInterface $workspace, EntityTypeManagerInterface $entity_type_manager, SequenceIndexInterface $sequence_index) {
+    $this->workspaceId = $workspace->id();
+    $this->entityTypeManager = $entity_type_manager;
+    $this->sequenceIndex = $sequence_index;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function includeEntities($include_entities) {
+    $this->includeEntities = $include_entities;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lastSeq($seq) {
+    $this->lastSeq = $seq;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNormal() {
+    $sequences = $this->sequenceIndex
+      ->useWorkspace($this->workspaceId)
+      ->getRange($this->lastSeq, NULL);
+
+    // Format the result array.
+    $changes = [];
+    foreach ($sequences as $sequence) {
+      // Get the document.
+      $revision = NULL;
+      if ($this->includeEntities == TRUE) {
+        $storage = $this->entityTypeManager->getStorage($sequence['entity_type_id']);
+        $storage->useWorkspace($this->workspaceId);
+        $revision = $storage->loadRevision($sequence['revision_id']);
+      }
+
+      $changes[$sequence['entity_uuid']] = [
+        'changes' => [
+          ['rev' => $sequence['revision_id']],
+        ],
+        'id' => $sequence['entity_id'],
+        'type' => $sequence['entity_type_id'],
+        'seq' => $sequence['seq'],
+      ];
+
+      // Include the document.
+      if ($this->includeEntities == TRUE) {
+        $changes[$sequence['entity_uuid']]['doc'] = $revision;
+      }
+    }
+
+    // Now when we have rebuilt the result array we need to ensure that the
+    // results array is still sorted on the sequence key, as in the index.
+    $return = array_values($changes);
+    usort($return, function($a, $b) {
+      return $a['seq'] - $b['seq'];
+    });
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLongpoll() {
+    $no_change = TRUE;
+    do {
+      $change = $this->sequenceIndex
+        ->useWorkspace($this->workspaceId)
+        ->getRange($this->lastSeq, NULL);
+      $no_change = empty($change) ? TRUE : FALSE;
+    } while ($no_change);
+    return $change;
+  }
+
+}
diff --git a/core/modules/workspace/src/Changes/ChangesFactory.php b/core/modules/workspace/src/Changes/ChangesFactory.php
new file mode 100644
index 0000000000..5834eaf48b
--- /dev/null
+++ b/core/modules/workspace/src/Changes/ChangesFactory.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\workspace\Changes;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Drupal\workspace\Index\SequenceIndexInterface;
+
+/**
+ * Class ChangesFactory
+ */
+class ChangesFactory implements ChangesFactoryInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\workspace\Index\SequenceIndexInterface
+   */
+  protected $sequenceIndex;
+
+  /**
+   * @var \Drupal\workspace\Changes\Changes[]
+   */
+  protected $instances = [];
+
+  /**
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, SequenceIndexInterface $sequence_index) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->sequenceIndex = $sequence_index;
+  }
+  /**
+   * {@inheritdoc}
+   */
+  public function get(WorkspaceInterface $workspace) {
+    if (!isset($this->instances[$workspace->id()])) {
+      $this->instances[$workspace->id()] = new Changes(
+        $workspace,
+        $this->entityTypeManager,
+        $this->sequenceIndex
+      );
+    }
+    return $this->instances[$workspace->id()];
+  }
+
+}
diff --git a/core/modules/workspace/src/Changes/ChangesFactoryInterface.php b/core/modules/workspace/src/Changes/ChangesFactoryInterface.php
new file mode 100644
index 0000000000..d885469b46
--- /dev/null
+++ b/core/modules/workspace/src/Changes/ChangesFactoryInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\workspace\Changes;
+
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Interface ChangesFactoryInterface
+ */
+interface ChangesFactoryInterface {
+
+  /**
+   * Constructs a new Changes instance.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *
+   * @return \Drupal\workspace\Changes\ChangesInterface
+   */
+  public function get(WorkspaceInterface $workspace);
+
+}
diff --git a/core/modules/workspace/src/Changes/ChangesInterface.php b/core/modules/workspace/src/Changes/ChangesInterface.php
new file mode 100644
index 0000000000..a86b069929
--- /dev/null
+++ b/core/modules/workspace/src/Changes/ChangesInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\workspace\Changes;
+
+/**
+ * Define and build a list of changes for a Workspace.
+ */
+interface ChangesInterface {
+
+  /**
+   * Set the flag for including entities in the list of changes.
+   *
+   * @param bool $include_entities
+   *   Whether to include entities in the list of changes.
+   *
+   * @return \Drupal\workspace\Changes\ChangesInterface
+   *   Returns $this.
+   */
+  public function includeEntities($include_entities);
+
+  /**
+   * Sets from what sequence number to check for changes.
+   *
+   * @param int $seq
+   *   The sequence ID to start including changes from. Result includes $seq.
+   *
+   * @return \Drupal\workspace\Changes\ChangesInterface
+   *   Returns $this.
+   */
+  public function lastSeq($seq);
+
+  /**
+   * Return the changes in a 'normal' way.
+   *
+   * @return array
+   */
+  public function getNormal();
+
+  /**
+   * Return the changes with a 'longpoll'.
+   *
+   * @return array
+   */
+  public function getLongpoll();
+
+}
diff --git a/core/modules/workspace/src/Controller/DeploymentController.php b/core/modules/workspace/src/Controller/DeploymentController.php
new file mode 100644
index 0000000000..bbc8904723
--- /dev/null
+++ b/core/modules/workspace/src/Controller/DeploymentController.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\workspace\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\workspace\Entity\Workspace;
+use Drupal\workspace\Form\DeploymentForm;
+use Drupal\workspace\UpstreamManager;
+use Drupal\workspace\WorkspaceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Class DeploymentController
+ */
+class DeploymentController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * @var \Drupal\workspace\UpstreamManager
+   */
+  protected $upstreamManager;
+
+  /**
+   * DeploymentController constructor.
+   *
+   * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   * @param \Drupal\workspace\UpstreamManager $upstream_manager
+   */
+  public function __construct(WorkspaceManagerInterface $workspace_manager, FormBuilderInterface $form_builder, UpstreamManager $upstream_manager) {
+    $this->workspaceManager = $workspace_manager;
+    $this->formBuilder = $form_builder;
+    $this->upstreamManager = $upstream_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('workspace.manager'),
+      $container->get('form_builder'),
+      $container->get('workspace.upstream_manager')
+    );
+  }
+
+  /**
+   * @return array
+   */
+  public function workspaces($workspace = NULL) {
+    $active_workspace = Workspace::load($workspace);
+    $form = $this->formBuilder->getForm(DeploymentForm::class, $active_workspace);
+
+    return [
+      '#type' => 'details',
+      '#title' => $this->t('Deploy content'),
+      '#open' => TRUE,
+      'from' => [
+        '#prefix' => '<p>',
+        '#markup' => $this->t('Update %from from %to or deploy to %to.', [
+          '%from' => $active_workspace->label(),
+          '%to' => $this->upstreamManager
+            ->createInstance($active_workspace->get('upstream')->value)
+            ->getLabel()
+        ]),
+        '#suffix' => '</p>',
+      ],
+      'form' => $form,
+    ];
+  }
+
+}
diff --git a/core/modules/workspace/src/Entity/Form/WorkspaceForm.php b/core/modules/workspace/src/Entity/Form/WorkspaceForm.php
new file mode 100644
index 0000000000..15819fb96e
--- /dev/null
+++ b/core/modules/workspace/src/Entity/Form/WorkspaceForm.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\workspace\Entity\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Entity\EntityConstraintViolationListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Form controller for the workspace edit forms.
+ */
+class WorkspaceForm extends ContentEntityForm {
+
+  /**
+   * The workspace content entity.
+   *
+   * @var \Drupal\workspace\Entity\WorkspaceInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $workspace = $this->entity;
+
+    if ($this->operation == 'edit') {
+      $form['#title'] = $this->t('Edit workspace %label', ['%label' => $workspace->label()]);
+    }
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#maxlength' => 255,
+      '#default_value' => $workspace->label(),
+      '#required' => TRUE,
+    ];
+
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#title' => $this->t('Workspace ID'),
+      '#maxlength' => 255,
+      '#default_value' => $workspace->id(),
+      '#disabled' => !$workspace->isNew(),
+      '#machine_name' => [
+        'exists' => '\Drupal\workspace\Entity\Workspace::load',
+      ],
+      '#element_validate' => [],
+    ];
+
+    $upstreams = [];
+    $upstream_manager = \Drupal::service('workspace.upstream_manager');
+    $upstream_definitions = $upstream_manager->getDefinitions();
+    foreach ($upstream_definitions as $upstream_definition) {
+      /** @var \Drupal\workspace\UpstreamInterface $instance */
+      $instance = $upstream_manager->createInstance($upstream_definition['id']);
+      $upstreams[$instance->getPluginId()] = $instance->getLabel();
+    }
+    $form['upstream'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Default upstream'),
+      '#default_value' => $workspace->get('upstream')->value,
+      '#options' => $upstreams,
+    ];
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditedFieldNames(FormStateInterface $form_state) {
+    return array_merge([
+      'label',
+      'id',
+    ], parent::getEditedFieldNames($form_state));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
+    // Manually flag violations of fields not handled by the form display. This
+    // is necessary as entity form displays only flag violations for fields
+    // contained in the display.
+    $field_names = [
+      'label',
+      'id',
+      'upstream'
+    ];
+    foreach ($violations->getByFields($field_names) as $violation) {
+      list($field_name) = explode('.', $violation->getPropertyPath(), 2);
+      $form_state->setErrorByName($field_name, $violation->getMessage());
+    }
+    parent::flagViolations($violations, $form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $workspace = $this->entity;
+    $status = $workspace->save();
+
+    \Drupal::service('workspace.upstream_manager')->clearCachedDefinitions();
+
+    $info = ['%info' => $workspace->label()];
+    $context = ['@type' => $workspace->bundle(), '%info' => $workspace->label()];
+    $logger = $this->logger('workspace');
+
+    if ($status == SAVED_UPDATED) {
+      $logger->notice('@type: updated %info.', $context);
+      drupal_set_message($this->t('Workspace %info has been updated.', $info));
+    }
+    else {
+      $logger->notice('@type: added %info.', $context);
+      drupal_set_message($this->t('Workspace %info has been created.', $info));
+    }
+
+    if ($workspace->id()) {
+      $form_state->setValue('id', $workspace->id());
+      $form_state->set('id', $workspace->id());
+      $redirect = $this->currentUser()->hasPermission('administer workspaces') ? $workspace->toUrl('collection') : Url::fromRoute('<front>');
+      $form_state->setRedirectUrl($redirect);
+    }
+    else {
+      drupal_set_message($this->t('The workspace could not be saved.'), 'error');
+      $form_state->setRebuild();
+    }
+  }
+
+}
diff --git a/core/modules/workspace/src/Entity/ReplicationLog.php b/core/modules/workspace/src/Entity/ReplicationLog.php
new file mode 100644
index 0000000000..f1415e7553
--- /dev/null
+++ b/core/modules/workspace/src/Entity/ReplicationLog.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Drupal\workspace\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * The replication log entity type.
+ *
+ * @ContentEntityType(
+ *   id = "replication_log",
+ *   label = @Translation("Replication log"),
+ *   handlers = {
+ *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *   },
+ *   base_table = "replication_log",
+ *   revision_table = "replication_log_revision",
+ *   fieldable = FALSE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "revision" = "revision_id",
+ *   },
+ *   local = TRUE,
+ * )
+ */
+class ReplicationLog extends ContentEntityBase implements ReplicationLogInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHistory() {
+    return $this->get('history')->getValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setHistory($history) {
+    $histories = array_merge([$history], $this->getHistory());
+    $this->set('history', $histories);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSessionId() {
+    return $this->get('session_id')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSessionId($session_id) {
+    $this->set('session_id', $session_id);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceLastSeq() {
+    return $this->get('source_last_seq')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSourceLastSeq($source_last_seq) {
+    $this->set('source_last_seq', $source_last_seq);
+    return $this;
+  }
+
+  /**
+   * @param $id string
+   * @return \Drupal\workspace\Entity\ReplicationLogInterface
+   */
+  public static function loadOrCreate($id) {
+    $entities = \Drupal::entityTypeManager()
+      ->getStorage('replication_log')
+      ->loadByProperties(['uuid' => $id]);
+    if (!empty($entities)) {
+      return reset($entities);
+    }
+    else {
+      return static::create(['uuid' => $id]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields['id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('ID'))
+      ->setDescription(t('The ID of the replication log entity.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['uuid'] = BaseFieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The UUID of the replication log entity.'))
+      ->setReadOnly(TRUE);
+
+    $fields['revision_id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('Revision ID'))
+      ->setDescription(t('The local revision ID of the replication log entity.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['history'] = BaseFieldDefinition::create('replication_history')
+      ->setLabel(t('Replication log history'))
+      ->setDescription(t('The version id of the test entity.'))
+      ->setReadOnly(TRUE)
+      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+
+    $fields['session_id'] = BaseFieldDefinition::create('uuid')
+      ->setLabel(t('Replication session ID'))
+      ->setDescription(t('The unique session ID of the last replication. Shortcut to the session_id in the last history item.'))
+      ->setReadOnly(TRUE);
+
+    $fields['source_last_seq'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Last processed checkpoint'))
+      ->setDescription(t('The last processed checkpoint. Shortcut to the source_last_seq in the last history item.'))
+      ->setReadOnly(TRUE);
+
+    $fields['ok'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('ok'))
+      ->setDescription(t('Replication status'))
+      ->setDefaultValue(TRUE)
+      ->setReadOnly(TRUE);
+
+    return $fields;
+  }
+
+}
diff --git a/core/modules/workspace/src/Entity/ReplicationLogInterface.php b/core/modules/workspace/src/Entity/ReplicationLogInterface.php
new file mode 100644
index 0000000000..20a39aa547
--- /dev/null
+++ b/core/modules/workspace/src/Entity/ReplicationLogInterface.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\workspace\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+interface ReplicationLogInterface extends ContentEntityInterface {
+
+  /**
+   * Gets the entire history.
+   *
+   * @return array
+   *   List of history values.
+   */
+  public function getHistory();
+
+  /**
+   * Sets the entire history.
+   *
+   * @param array $history
+   *   List containing replication history items.
+   *
+   * @return $this
+   */
+  public function setHistory($history);
+
+  /**
+   * Gets the session id.
+   *
+   * @return string
+   *   The session id.
+   */
+  public function getSessionId();
+
+  /**
+   * Sets the session id.
+   *
+   * @param string $session_id
+   *   The session id to set.
+   *
+   * @return $this
+   */
+  public function setSessionId($session_id);
+
+  /**
+   * Gets the last processed checkpoint.
+   *
+   * @return string
+   *   The last processed checkpoint.
+   */
+  public function getSourceLastSeq();
+
+  /**
+   * Sets the session id.
+   *
+   * @param string $source_last_seq
+   *   The last processed checkpoint.
+   *
+   * @return $this
+   */
+  public function setSourceLastSeq($source_last_seq);
+
+}
diff --git a/core/modules/workspace/src/Entity/Workspace.php b/core/modules/workspace/src/Entity/Workspace.php
new file mode 100644
index 0000000000..5c8ee09125
--- /dev/null
+++ b/core/modules/workspace/src/Entity/Workspace.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\workspace\Entity;
+
+use Drupal\Core\Entity\Entity;
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\user\UserInterface;
+
+/**
+ * The workspace entity class.
+ *
+ * @ContentEntityType(
+ *   id = "workspace",
+ *   label = @Translation("Workspace"),
+ *   bundle_label = @Translation("Workspace type"),
+ *   handlers = {
+ *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "list_builder" = "\Drupal\workspace\WorkspaceListBuilder",
+ *     "access" = "Drupal\workspace\WorkspaceAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "\Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
+ *     },
+ *     "form" = {
+ *       "default" = "\Drupal\workspace\Entity\Form\WorkspaceForm",
+ *       "add" = "\Drupal\workspace\Entity\Form\WorkspaceForm",
+ *       "edit" = "\Drupal\workspace\Entity\Form\WorkspaceForm",
+ *     },
+ *   },
+ *   admin_permission = "administer workspaces",
+ *   base_table = "workspace",
+ *   revision_table = "workspace_revision",
+ *   data_table = "workspace_field_data",
+ *   revision_data_table = "workspace_field_revision",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "revision" = "revision_id",
+ *     "uuid" = "uuid",
+ *     "label" = "label",
+ *     "uid" = "uid",
+ *     "created" = "created"
+ *   },
+ *   links = {
+ *     "add-form" = "/admin/structure/workspace/add",
+ *     "edit-form" = "/admin/structure/workspace/{workspace}/edit",
+ *     "activate-form" = "/admin/structure/workspace/{workspace}/activate",
+ *     "deployment-form" = "/admin/structure/workspace/{workspace}/deployment",
+ *     "collection" = "/admin/structure/workspace",
+ *   },
+ *   field_ui_base_route = "entity.workspace.collection",
+ * )
+ */
+class Workspace extends ContentEntityBase implements WorkspaceInterface {
+
+  use EntityChangedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['id'] = BaseFieldDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Workaspace ID'))
+      ->setDescription(new TranslatableMarkup('The workspace ID.'))
+      ->setSetting('max_length', 128)
+      ->setRequired(TRUE)
+      ->addPropertyConstraints('value', ['Regex' => ['pattern' => '/^[\da-z_$()+-\/]*$/']]);
+
+    $fields['label'] = BaseFieldDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Workaspace name'))
+      ->setDescription(new TranslatableMarkup('The workspace name.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('max_length', 128)
+      ->setRequired(TRUE);
+
+    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(new TranslatableMarkup('Owner'))
+      ->setDescription(new TranslatableMarkup('The workspace owner.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setDefaultValueCallback('Drupal\workspace\Entity\Workspace::getCurrentUserId');
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(new TranslatableMarkup('Changed'))
+      ->setDescription(new TranslatableMarkup('The time that the workspace was last edited.'))
+      ->setRevisionable(TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(new TranslatableMarkup('Created'))
+      ->setDescription(new TranslatableMarkup('The UNIX timestamp of when the workspace has been created.'));
+
+    $fields['upstream'] = BaseFieldDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Assign default target workspace'))
+      ->setDescription(new TranslatableMarkup('The workspace to push to and pull from.'))
+      ->setRevisionable(TRUE)
+      ->setRequired(TRUE)
+      ->setDefaultValueCallback('Drupal\workspace\Entity\Workspace::getActiveWorkspaceId');
+
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUpdateSeq() {
+    return \Drupal::service('workspace.entity_index.sequence')->useWorkspace($this->id())->getLastSequenceId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($created) {
+    $this->set('created', (int) $created);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getStartTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('uid')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('uid', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('uid')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * 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()];
+  }
+
+  /**
+   * Default value callback for 'upstream' base field definition.
+   *
+   * @see ::baseFieldDefinitions()
+   *
+   * @return array
+   *   An array of default values.
+   */
+  public static function getActiveWorkspaceId() {
+    return ['workspace:' . \Drupal::service('workspace.manager')->getActiveWorkspace()];
+  }
+
+}
diff --git a/core/modules/workspace/src/Entity/WorkspaceInterface.php b/core/modules/workspace/src/Entity/WorkspaceInterface.php
new file mode 100644
index 0000000000..93cc55d37a
--- /dev/null
+++ b/core/modules/workspace/src/Entity/WorkspaceInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\workspace\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
+
+interface WorkspaceInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  /**
+   * Returns the last sequence ID in the workspace's sequence index.
+   *
+   * @return float
+   */
+  public function getUpdateSeq();
+
+  /**
+   * Sets the workspace creation timestamp.
+   *
+   * @param int $timestamp
+   *   The workspace creation timestamp.
+   *
+   * @return $this
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Returns the workspace creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the workspace.
+   */
+  public function getStartTime();
+
+}
diff --git a/core/modules/workspace/src/EntityAccess.php b/core/modules/workspace/src/EntityAccess.php
new file mode 100644
index 0000000000..9b02b49783
--- /dev/null
+++ b/core/modules/workspace/src/EntityAccess.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Service wrapper for hooks relating to entity access control.
+ */
+class EntityAccess implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * @var int
+   *
+   * The ID of the default workspace, which has special permission handling.
+   */
+  protected $defaultWorkspaceId;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * Constructs a new EntityAccess.
+   *
+   * @param EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   * @param WorkspaceManagerInterface $workspace_manager
+   *   The workspace manager service.
+   * @param int $default_workspace
+   *   The ID of the default workspace.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, $default_workspace) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->workspaceManager = $workspace_manager;
+    $this->defaultWorkspaceId = $default_workspace;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('workspace.manager'),
+      $container->getParameter('workspace.default')
+    );
+  }
+  }
+
+  /**
+   * Hook bridge;
+   *
+   * @see hook_entity_access()
+   *
+   * @param EntityInterface $entity
+   * @param string $operation
+   * @param AccountInterface $account
+   *
+   * @return AccessResult
+   */
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    // Workspaces themselves are handled by another hook. Ignore them here.
+    if ($entity->getEntityTypeId() == 'workspace') {
+      return AccessResult::neutral();
+    }
+
+    /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */
+    $workspace_manager = \Drupal::service('workspace.manager');
+    if ($workspace_manager->entityCanBelongToWorkspaces($entity)
+      && $entity->workspace->target_id != \Drupal::getContainer()->getParameter('workspace.default')) {
+      $active_workspace = $workspace_manager->getActiveWorkspace();
+      $result = \Drupal::entityTypeManager()
+        ->getStorage('content_workspace')
+        ->getQuery()
+        ->allRevisions()
+        ->condition('content_entity_type_id', $entity->getEntityTypeId())
+        ->condition('content_entity_id', $entity->id())
+        ->condition('content_entity_revision_id', $entity->getRevisionId())
+        ->condition('workspace', $active_workspace)
+        ->execute();
+      if (empty($result)) {
+        return AccessResult::forbidden();
+      }
+    }
+
+    return $this->bypassAccessResult($account);
+  }
+
+  /**
+   * Hook bridge;
+   *
+   * @see hook_entity_create_access()
+   *
+   * @param AccountInterface $account
+   * @param array $context
+   * @param $entity_bundle
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   */
+  public function entityCreateAccess(AccountInterface $account, array $context, $entity_bundle) {
+
+    // Workspaces themselves are handled by another hook. Ignore them here.
+    if ($entity_bundle == 'workspace') {
+      return AccessResult::neutral();
+    }
+
+    return $this->bypassAccessResult($account);
+  }
+
+  /**
+   * @param AccountInterface $account
+   * @return AccessResult
+   */
+  protected function bypassAccessResult(AccountInterface $account) {
+    // This approach assumes that the current "global" active workspace is
+    // correct, ie, if you're "in" a given workspace then you get ALL THE PERMS
+    // to ALL THE THINGS! That's why this is a dangerous permission.
+    $active_workspace = $this->workspaceManager->getActiveWorkspace(TRUE);
+
+    return AccessResult::allowedIfHasPermission($account, 'bypass_entity_access_workspace_' . $active_workspace->id())
+      ->orIf(
+        AccessResult::allowedIf($active_workspace->getOwnerId() == $account->id())
+          ->andIf(AccessResult::allowedIfHasPermission($account, 'bypass entity access own workspace'))
+      );
+  }
+
+  /**
+   * Returns an array of workspace-specific permissions.
+   *
+   * Note: This approach assumes that a site will have only a small number
+   * of workspace entities, under a dozen. If there are many dozens of
+   * workspaces defined then this approach will have scaling issues.
+   *
+   * @return array
+   *   The workspace permissions.
+   */
+  public function workspacePermissions() {
+    $perms = [];
+
+    foreach ($this->getAllWorkspaces() as $workspace) {
+      $perms += $this->createWorkspaceViewPermission($workspace)
+      + $this->createWorkspaceEditPermission($workspace)
+      + $this->createWorkspaceDeletePermission($workspace)
+      + $this->createWorkspaceBypassPermission($workspace);
+    }
+
+    return $perms;
+  }
+
+  /**
+   * Returns a list of all workspace entities in the system.
+   *
+   * @return \Drupal\workspace\Entity\WorkspaceInterface[]
+   *   An array of workspace entities, keyed by their IDs.
+   */
+  protected function getAllWorkspaces() {
+    return $this->entityTypeManager->getStorage('workspace')->loadMultiple();
+  }
+
+  /**
+   * Derives the view permission for a specific workspace.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace from which to derive the permission.
+   * @return array
+   *   A single-item array with the permission to define.
+   */
+  protected function createWorkspaceViewPermission(WorkspaceInterface $workspace) {
+    $perms['view_workspace_' . $workspace->id()] = [
+      'title' => $this->t('View the %workspace workspace', ['%workspace' => $workspace->label()]),
+      'description' => $this->t('View the %workspace workspace and content within it', ['%workspace' => $workspace->label()]),
+    ];
+
+    return $perms;
+  }
+
+  /**
+   * Derives the edit permission for a specific workspace.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace from which to derive the permission.
+   * @return array
+   *   A single-item array with the permission to define.
+   */
+  protected function createWorkspaceEditPermission(WorkspaceInterface $workspace) {
+    $perms['update_workspace_' . $workspace->id()] = [
+      'title' => $this->t('Edit the %workspace workspace', ['%workspace' => $workspace->label()]),
+      'description' => $this->t('Edit the %workspace workspace itself', ['%workspace' => $workspace->label()]),
+    ];
+
+    return $perms;
+  }
+
+  /**
+   * Derives the delete permission for a specific workspace.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace from which to derive the permission.
+   * @return array
+   *   A single-item array with the permission to define.
+   */
+  protected function createWorkspaceDeletePermission(WorkspaceInterface $workspace) {
+    $perms['delete_workspace_' . $workspace->id()] = [
+      'title' => $this->t('Delete the %workspace workspace', ['%workspace' => $workspace->label()]),
+      'description' => $this->t('View the %workspace workspace and all content within it', ['%workspace' => $workspace->label()]),
+    ];
+
+    return $perms;
+  }
+
+  /**
+   * Derives the delete permission for a specific workspace.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace from which to derive the permission.
+   * @return array
+   *   A single-item array with the permission to define.
+   */
+  protected function createWorkspaceBypassPermission(WorkspaceInterface $workspace) {
+    $perms['bypass_entity_access_workspace_' . $workspace->id()] = [
+      'title' => $this->t('Bypass content entity access in %workspace workspace', ['%workspace' => $workspace->label()]),
+      'description' => $this->t('Allow all Edit/Update/Delete permissions for all content in the %workspace workspace', ['%workspace' => $workspace->label()]),
+      'restrict access' => TRUE,
+    ];
+
+    return $perms;
+  }
+
+}
diff --git a/core/modules/workspace/src/Form/DeploymentForm.php b/core/modules/workspace/src/Form/DeploymentForm.php
new file mode 100644
index 0000000000..571110affe
--- /dev/null
+++ b/core/modules/workspace/src/Form/DeploymentForm.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\workspace\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workspace\Entity\Workspace;
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Class DeploymentForm
+ */
+class DeploymentForm extends FormBase {
+
+  /**
+   * @inheritDoc
+   */
+  public function getFormId() {
+    return 'workspace_deployment_form';
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, WorkspaceInterface $workspace = NULL) {
+    if ('workspace:' . $workspace->id() == $workspace->get('upstream')->value) {
+      return [];
+    }
+
+    $upstream_plugin = \Drupal::service('workspace.upstream_manager')->createInstance($workspace->get('upstream')->value);
+    $form['workspace_id'] = [
+      '#type' => 'hidden',
+      '#value' => $workspace->id(),
+    ];
+
+    $form['deploy'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Deploy to %upstream', ['%upstream' => $upstream_plugin->getLabel()]),
+      '#attributes' => [
+        'class' => ['primary', 'button--primary'],
+      ],
+      '#weight' => 1,
+    ];
+
+    $form['update'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Update'),
+      '#submit' => ['::updateHandler'],
+      '#weight' => 0,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public function updateHandler(array &$form, FormStateInterface $form_state) {
+    $workspace = Workspace::load($form_state->getValue('workspace_id'));
+    $upstream_manager = \Drupal::service('workspace.upstream_manager');
+    try {
+      \Drupal::service('workspace.replication_manager')->replicate(
+        $upstream_manager->createInstance($workspace->get('upstream')->value),
+        $upstream_manager->createInstance('workspace:' . $workspace->id())
+      );
+      drupal_set_message('Update successful.');
+    }
+    catch (\Exception $e) {
+      drupal_set_message('Deployment error', 'error');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $workspace = Workspace::load($form_state->getValue('workspace_id'));
+    $upstream_manager = \Drupal::service('workspace.upstream_manager');
+    try {
+      \Drupal::service('workspace.replication_manager')->replicate(
+        $upstream_manager->createInstance('workspace:' . $workspace->id()),
+        $upstream_manager->createInstance($workspace->get('upstream')->value)
+      );
+      drupal_set_message('Successful deployment.');
+    }
+    catch (\Exception $e) {
+      drupal_set_message('Deployment error', 'error');
+    }
+  }
+
+}
diff --git a/core/modules/workspace/src/Form/WorkspaceActivateForm.php b/core/modules/workspace/src/Form/WorkspaceActivateForm.php
new file mode 100644
index 0000000000..1ccdda6150
--- /dev/null
+++ b/core/modules/workspace/src/Form/WorkspaceActivateForm.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\workspace\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Handle activation of a workspace on administrative pages.
+ */
+class WorkspaceActivateForm extends WorkspaceActivateFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'activate';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, WorkspaceInterface $workspace = NULL) {
+    $form['workspace_id'] = [
+      '#type' => 'hidden',
+      '#value' => $workspace->id(),
+    ];
+
+    $form['instruction'] = [
+      '#type' => 'markup',
+      '#prefix' => '<p>',
+      '#markup' => $this->t('Would you like to activate the %workspace workspace?', ['%workspace' => $workspace->label()]),
+      '#suffix' => '</p>',
+    ];
+
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => 'Activate',
+    ];
+
+    $form['#title'] = $this->t('Activate workspace %label', ['%label' => $workspace->label()]);
+
+    return $form;
+  }
+
+}
diff --git a/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php b/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php
new file mode 100644
index 0000000000..619359be01
--- /dev/null
+++ b/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\workspace\Form;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workspace\WorkspaceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The base class for forms that activate a workspace.
+ *
+ * Use this class as the base for any form that switches the active workspace.
+ * This abstraction handles form validation and submission.
+ */
+abstract class WorkspaceActivateFormBase extends FormBase {
+
+  /**
+   * @var \Drupal\multiversion\Workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('workspace.manager'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  public function __construct(WorkspaceManagerInterface $workspace_manager, EntityTypeManagerInterface $entity_type_manager) {
+    $this->workspaceManager = $workspace_manager;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $id = $form_state->getValue('workspace_id');
+
+    // Ensure we are given an ID.
+    if (!$id) {
+      $form_state->setErrorByName('workspace_id', 'The workspace ID is required.');
+    }
+
+    // Ensure the workspace by that id exists.
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $workspace */
+    $workspace = $this->entityTypeManager->getStorage('workspace')->load($id);
+    if (!$workspace) {
+      $form_state->setErrorByName('workspace_id', 'This workspace no longer exists.');
+    }
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $id = $form_state->getValue('workspace_id');
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $workspace */
+    $workspace = $this->entityTypeManager->getStorage('workspace')->load($id);
+
+    try {
+      $this->workspaceManager->setActiveWorkspace($workspace);
+      $form_state->setRedirect('<front>');
+    }
+    catch (\Exception $e) {
+      watchdog_exception('Workspace', $e);
+      drupal_set_message($e->getMessage(), 'error');
+    }
+  }
+
+}
diff --git a/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php b/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php
new file mode 100644
index 0000000000..2ad2cafa92
--- /dev/null
+++ b/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\workspace\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Switcher for to activate a different workspace.
+ *
+ * This is a separate form for each workspace rather than one big form with
+ * many buttons for scaling reasons. For example, this form may show up in a
+ * toolbar. We may want to show just a subset of workspaces to switch to, maybe
+ * access control, etc. This approach keeps that logic out of the switching
+ * process itself.
+ */
+class WorkspaceSwitcherForm extends WorkspaceActivateFormBase {
+
+  /**
+   * Hack to allow us to show this form multiple times on a page.
+   *
+   * @see https://www.drupal.org/node/766146
+   *
+   * @var int
+   */
+  protected static $formCounter = 1;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'workspace_switcher_form_' . static::$formCounter++;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, WorkspaceInterface $workspace = NULL) {
+    // @todo this form is identical to WorkspaceActivateForm except for this method; can we consolidate forms?
+    $form['workspace_id'] = [
+      '#type' => 'hidden',
+      '#value' => $workspace->id(),
+    ];
+
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => $workspace->label(),
+    ];
+
+    $active_workspace = $this->workspaceManager->getActiveWorkspace();
+    if ($active_workspace === $workspace->id()) {
+      $form['submit']['#attributes']['class'] = ['is-active'];
+    }
+
+    return $form;
+  }
+
+}
diff --git a/core/modules/workspace/src/Index/SequenceIndex.php b/core/modules/workspace/src/Index/SequenceIndex.php
new file mode 100644
index 0000000000..aaa0ed567a
--- /dev/null
+++ b/core/modules/workspace/src/Index/SequenceIndex.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\workspace\Index;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\workspace\KeyValueStore\KeyValueSortedSetFactoryInterface;
+use Drupal\Workspace\WorkspaceManagerInterface;
+
+/**
+ * Class SequenceIndex
+ */
+class SequenceIndex implements SequenceIndexInterface {
+
+  /**
+   * @var string
+   */
+  protected $collectionPrefix = 'workspace.sequence_index.';
+
+  /**
+   * @var string
+   */
+  protected $workspaceId;
+
+  /**
+   * @var \Drupal\workspace\KeyValueStore\KeyValueSortedSetFactoryInterface
+   */
+  protected $sortedSetFactory;
+
+  /**
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * @param \Drupal\workspace\KeyValueStore\KeyValueSortedSetFactoryInterface $sorted_set_factory
+   * @param \Drupal\Workspace\WorkspaceManagerInterface $workspace_manager
+   */
+  public function __construct(KeyValueSortedSetFactoryInterface $sorted_set_factory, WorkspaceManagerInterface $workspace_manager) {
+    $this->sortedSetFactory = $sorted_set_factory;
+    $this->workspaceManager = $workspace_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function useWorkspace($id) {
+    $this->workspaceId = $id;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(ContentEntityInterface $entity) {
+    $workspace_id = NULL;
+    $record = $this->buildRecord($entity);
+    if ($entity->getEntityType()->get('workspace') === FALSE) {
+      $workspace_id = 0;
+    }
+    $this->sortedSetStore($workspace_id)->add($record['seq'], $record);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRange($start, $stop = NULL, $inclusive = TRUE) {
+    $range = $this->sortedSetStore()->getRange($start, $stop, $inclusive);
+    if (empty($range)) {
+      $range = $this->sortedSetStore(0)->getRange($start, $stop, $inclusive);
+    }
+    return $range;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastSequenceId() {
+    $max_key = $this->sortedSetStore()->getMaxKey();
+    if (empty($max_key)) {
+      $max_key = $this->sortedSetStore(0)->getMaxKey();
+    }
+    return $max_key;
+  }
+
+  /**
+   * @param $workspace_id
+   *
+   * @return \Drupal\workspace\KeyValueStore\KeyValueStoreSortedSetInterface
+   */
+  protected function sortedSetStore($workspace_id = NULL) {
+    if (!$workspace_id) {
+      $workspace_id = $this->workspaceId ?: $this->workspaceManager->getActiveWorkspace()->id();
+    }
+    return $this->sortedSetFactory->get($this->collectionPrefix . $workspace_id);
+  }
+
+  /**
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   * @return array
+   */
+  protected function buildRecord(ContentEntityInterface $entity) {
+    return [
+      'entity_type_id' => $entity->getEntityTypeId(),
+      'entity_id' => $entity->id(),
+      'entity_uuid' => $entity->uuid(),
+      'revision_id' => $entity->getRevisionId(),
+      'seq' => (int) (microtime(TRUE) * 1000000),
+    ];
+  }
+
+}
diff --git a/core/modules/workspace/src/Index/SequenceIndexInterface.php b/core/modules/workspace/src/Index/SequenceIndexInterface.php
new file mode 100644
index 0000000000..0c58e2f2ca
--- /dev/null
+++ b/core/modules/workspace/src/Index/SequenceIndexInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\workspace\Index;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Interface SequenceIndexInterface
+ */
+interface SequenceIndexInterface {
+
+  /**
+   * @param $id
+   *
+   * @return $this
+   */
+  public function useWorkspace($id);
+
+  /**
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *
+   * @return $this
+   */
+  public function add(ContentEntityInterface $entity);
+
+  /**
+   * @param float $start
+   * @param float $stop
+   * @param bool $inclusive
+   *
+   * @return array
+   */
+  public function getRange($start, $stop = NULL, $inclusive = TRUE);
+
+  /**
+   * @return float
+   */
+  public function getLastSequenceId();
+
+}
diff --git a/core/modules/workspace/src/KeyValueStore/DatabaseStorageSortedSet.php b/core/modules/workspace/src/KeyValueStore/DatabaseStorageSortedSet.php
new file mode 100644
index 0000000000..9adafbc581
--- /dev/null
+++ b/core/modules/workspace/src/KeyValueStore/DatabaseStorageSortedSet.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Drupal\workspace\KeyValueStore;
+
+use Drupal\Component\Serialization\SerializationInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\SchemaObjectExistsException;
+
+/**
+ * Defines the default key/value implementation for sorted collections.
+ *
+ * This key/value store implementation uses the database to store key/value
+ * data in a sorted collection.
+ */
+class DatabaseStorageSortedSet implements KeyValueStoreSortedSetInterface {
+
+  /**
+   * The name of the collection holding key and value pairs.
+   *
+   * @var string
+   */
+  protected $collection;
+
+  /**
+   * The serialization class to use.
+   *
+   * @var \Drupal\Component\Serialization\SerializationInterface
+   */
+  protected $serializer;
+
+  /**
+   * The database connection to use.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * The name of the SQL table to use, defaults to key_value_sorted.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * Constructs the database key/value implementation for sorted collections.
+   *
+   * @param string $collection
+   *   The name of the collection holding key and value pairs.
+   * @param \Drupal\Component\Serialization\SerializationInterface $serializer
+   *   The serialization class to use.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection to use.
+   * @param string $table
+   *   The name of the SQL table to use, defaults to key_value_sorted.
+   */
+  public function __construct($collection, SerializationInterface $serializer, Connection $connection, $table = 'key_value_sorted') {
+    $this->collection = $collection;
+    $this->serializer = $serializer;
+    $this->connection = $connection;
+    $this->table = $table;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add($key, $value) {
+    return $this->addMultiple([[$key => $value]]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addMultiple(array $pairs) {
+    foreach ($pairs as $pair) {
+      foreach ($pair as $key => $value) {
+        $try_again = FALSE;
+        try {
+          $encoded_value = $this->serializer->encode($value);
+          $this->connection->merge($this->table)
+            ->fields([
+              'collection' => $this->collection,
+              'name' => $key,
+              'value' => $encoded_value,
+            ])
+            ->condition('collection', $this->collection)
+            ->condition('value', $encoded_value)
+            ->execute();
+        }
+        catch (\Exception $e) {
+          // If there was an exception, try to create the table.
+          if (!$try_again = $this->ensureTableExists()) {
+            // If the exception happened for other reason than the missing
+            // table, propagate the exception.
+            throw $e;
+          }
+        }
+        // Now that the table has been created, try again if necessary.
+        if ($try_again) {
+          $this->add($key, $value);
+        }
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCount() {
+    try {
+      return $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection', [
+        ':collection' => $this->collection
+      ])->fetchField();
+    }
+    catch (\Exception $e) {
+      $this->catchException($e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRange($start, $stop = NULL) {
+    try {
+      $query = $this->connection->select($this->table, 't')
+        ->fields('t', ['value'])
+        ->orderBy('name', 'ASC')
+        ->condition('collection', $this->collection)
+        ->condition('name', $start, '>=');
+
+      if (is_int($stop)) {
+        $query->condition('name', $stop, '<=');
+      }
+
+      $values = [];
+      foreach ($query->execute() as $item) {
+        $values[] = $this->serializer->decode($item->value);
+      }
+      return $values;
+    }
+    catch (\Exception $e) {
+      $this->catchException($e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMaxKey() {
+    try {
+      return $this->connection->query('SELECT MAX(name) FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection', [
+        ':collection' => $this->collection
+      ])->fetchField();
+    }
+    catch (\Exception $e) {
+      $this->catchException($e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMinKey() {
+    try {
+      return $this->connection->query('SELECT MIN(name) FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection', [
+        ':collection' => $this->collection
+      ])->fetchField();
+    }
+    catch (\Exception $e) {
+      $this->catchException($e);
+    }
+  }
+
+  /**
+   * Checks if the table exists and creates if not.
+   *
+   * @return bool
+   */
+  protected function ensureTableExists() {
+    try {
+      $database_schema = $this->connection->schema();
+      if (!$database_schema->tableExists($this->table)) {
+        $database_schema->createTable($this->table, $this->schemaDefinition());
+        return TRUE;
+      }
+    }
+    // If the table already exists, then attempting to recreate it will throw an
+    // exception. In this case just catch the exception and do nothing.
+    catch (SchemaObjectExistsException $e) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Act on an exception when the table might not have been created.
+   *
+   * If the table does not yet exist, that's fine, but if the table exists and
+   * something else caused the exception, then propagate it.
+   *
+   * @param \Exception $e
+   *   The exception.
+   *
+   * @throws \Exception
+   */
+  protected function catchException(\Exception $e) {
+    if ($this->connection->schema()->tableExists($this->table)) {
+      throw $e;
+    }
+  }
+
+  /**
+   * The schema definition for the sorted key-value list storage table.
+   *
+   * @return array
+   */
+  protected function schemaDefinition() {
+    return [
+      'description' => 'Sorted key-value list storage table.',
+      'fields' => [
+        'collection' => [
+          'description' => 'A named collection of key and value pairs.',
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => TRUE,
+          'default' => '',
+        ],
+        // KEY is an SQL reserved word, so use 'name' as the key's field name.
+        'name' => [
+          'description' => 'The index or score key for the value.',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'big',
+        ],
+        'value' => [
+          'description' => 'The value.',
+          'type' => 'blob',
+          'not null' => TRUE,
+          'size' => 'big',
+        ],
+      ],
+      'indexes' => [
+        'collection_name' => ['collection', 'name'],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/workspace/src/KeyValueStore/KeyValueDatabaseSortedSetFactory.php b/core/modules/workspace/src/KeyValueStore/KeyValueDatabaseSortedSetFactory.php
new file mode 100644
index 0000000000..b85d288bb1
--- /dev/null
+++ b/core/modules/workspace/src/KeyValueStore/KeyValueDatabaseSortedSetFactory.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\workspace\KeyValueStore;
+
+use Drupal\Component\Serialization\SerializationInterface;
+use Drupal\Core\Database\Connection;
+
+/**
+ * Defines the key/value store factory for the database backend.
+ */
+class KeyValueDatabaseSortedSetFactory implements KeyValueSortedSetFactoryInterface {
+
+  /**
+   * The serialization class to use.
+   *
+   * @var \Drupal\Component\Serialization\SerializationInterface
+   */
+  protected $serializer;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs this factory object.
+   *
+   * @param \Drupal\Component\Serialization\SerializationInterface $serializer
+   *   The serialization class to use.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The Connection object containing the key-value tables.
+   */
+  public function __construct(SerializationInterface $serializer, Connection $connection) {
+    $this->serializer = $serializer;
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($collection) {
+    return new DatabaseStorageSortedSet($collection, $this->serializer, $this->connection);
+  }
+
+}
diff --git a/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactory.php b/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactory.php
new file mode 100644
index 0000000000..94962594d9
--- /dev/null
+++ b/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactory.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\workspace\KeyValueStore;
+
+use Drupal\Core\KeyValueStore\KeyValueFactory;
+
+/**
+ * Defines the key/value store factory.
+ */
+class KeyValueSortedSetFactory extends KeyValueFactory implements KeyValueSortedSetFactoryInterface {
+
+  const DEFAULT_SERVICE = 'workspace.keyvalue.sorted_set.database';
+
+  const SPECIFIC_PREFIX = 'workspace_keyvalue_sorted_set_service_';
+
+  const DEFAULT_SETTING = 'workspace_keyvalue_sorted_set_default';
+
+}
diff --git a/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactoryInterface.php b/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactoryInterface.php
new file mode 100644
index 0000000000..d96fb55ec2
--- /dev/null
+++ b/core/modules/workspace/src/KeyValueStore/KeyValueSortedSetFactoryInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\workspace\KeyValueStore;
+
+/**
+ * Defines the sorted set key/value store factory interface.
+ */
+interface KeyValueSortedSetFactoryInterface {
+
+  /**
+   * Constructs a new sorted set key/value store for a given collection name.
+   *
+   * @param string $collection
+   *   The name of the collection holding key and value pairs.
+   *
+   * @return \Drupal\workspace\KeyValueStore\KeyValueStoreSortedSetInterface
+   *   An sorted set key/value store implementation for the given $collection.
+   */
+  public function get($collection);
+
+}
diff --git a/core/modules/workspace/src/KeyValueStore/KeyValueStoreSortedSetInterface.php b/core/modules/workspace/src/KeyValueStore/KeyValueStoreSortedSetInterface.php
new file mode 100644
index 0000000000..0f6f80e745
--- /dev/null
+++ b/core/modules/workspace/src/KeyValueStore/KeyValueStoreSortedSetInterface.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\workspace\KeyValueStore;
+
+/**
+ * Defines the interface for sorted data in a key/value store.
+ *
+ * An example would be sequential log of data ordered by key of current time.
+ * A value can just exist once, but a key could be used multiple times for items
+ * in the same position in a sequence. This is based on the Redis sorted sets.
+ */
+interface KeyValueStoreSortedSetInterface {
+
+  /**
+   * Add a single item to a collection.
+   *
+   * @param int $key
+   *   The key for the item, for example microtime(), which can be used to
+   *   generate a sequential value.
+   * @param mixed $value
+   *   The value of the item.
+   *
+   * @return $this
+   */
+  public function add($key, $value);
+
+  /**
+   * Add multiple items to a collection.
+   *
+   * @example [[1 => 'a'], [2 => 'b']].
+   *
+   * @param array[] $map
+   *   A map of keys and values to add.
+   *
+   * @return $this
+   */
+  public function addMultiple(array $map);
+
+  /**
+   * Get the highest key in a collection.
+   *
+   * @return int
+   *   The highest key in the collection.
+   */
+  public function getMaxKey();
+
+  /**
+   * Get the lowest key in in a collection.
+   *
+   * @return int
+   *   The lowest key in the collection.
+   */
+  public function getMinKey();
+
+  /**
+   * Get the number of items in a collection.
+   *
+   * @return int
+   *   The number of items in a collection.
+   */
+  public function getCount();
+
+  /**
+   * Get multiple items within a range of keys.
+   *
+   * @param int $start
+   *   The first key in the range.
+   * @param int $stop
+   *   The last key in the range.
+   *
+   * @return array
+   *   An array of items within the given range.
+   */
+  public function getRange($start, $stop = NULL);
+
+}
diff --git a/core/modules/workspace/src/Negotiator/DefaultWorkspaceNegotiator.php b/core/modules/workspace/src/Negotiator/DefaultWorkspaceNegotiator.php
new file mode 100644
index 0000000000..f60b6342f7
--- /dev/null
+++ b/core/modules/workspace/src/Negotiator/DefaultWorkspaceNegotiator.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\workspace\Negotiator;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Class DefaultWorkspaceNegotiator
+ */
+class DefaultWorkspaceNegotiator extends WorkspaceNegotiatorBase {
+
+  /**
+   * The default workspace ID.
+   *
+   * @var string
+   */
+  protected $defaultWorkspaceId;
+
+  /**
+   * Constructor.
+   *
+   * @param string $default_workspace_id
+   *   The default workspace ID.
+   */
+  public function __construct($default_workspace_id) {
+    $this->defaultWorkspaceId = $default_workspace_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Request $request) {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWorkspaceId(Request $request) {
+    return $this->defaultWorkspaceId;
+  }
+
+}
diff --git a/core/modules/workspace/src/Negotiator/ParamWorkspaceNegotiator.php b/core/modules/workspace/src/Negotiator/ParamWorkspaceNegotiator.php
new file mode 100644
index 0000000000..15bb410bcb
--- /dev/null
+++ b/core/modules/workspace/src/Negotiator/ParamWorkspaceNegotiator.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\workspace\Negotiator;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Class ParamWorkspaceNegotiator
+ */
+class ParamWorkspaceNegotiator extends WorkspaceNegotiatorBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Request $request) {
+    return is_string($request->query->get('workspace'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWorkspaceId(Request $request) {
+    return $request->query->get('workspace');
+  }
+
+}
diff --git a/core/modules/workspace/src/Negotiator/SessionWorkspaceNegotiator.php b/core/modules/workspace/src/Negotiator/SessionWorkspaceNegotiator.php
new file mode 100644
index 0000000000..81f9d4c100
--- /dev/null
+++ b/core/modules/workspace/src/Negotiator/SessionWorkspaceNegotiator.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\workspace\Negotiator;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Class SessionWorkspaceNegotiator
+ */
+class SessionWorkspaceNegotiator extends WorkspaceNegotiatorBase {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\PrivateTempStore
+   */
+  protected $tempstore;
+
+  /**
+   * The default workspace ID.
+   *
+   * @var string
+   */
+  protected $defaultWorkspaceId;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\user\PrivateTempStoreFactory $tempstore_factory
+   *   The tempstore factory.
+   * @param string $default_workspace_id
+   *   The default workspace ID.
+   */
+  public function __construct(AccountInterface $current_user, PrivateTempStoreFactory $tempstore_factory, $default_workspace_id) {
+    $this->currentUser = $current_user;
+    $this->tempstore = $tempstore_factory->get('workspace.negotiator.session');
+    $this->defaultWorkspaceId = $default_workspace_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Request $request) {
+    // This negotiator only applies if the current user is authenticated,
+    // i.e. a session exists.
+    return $this->currentUser->isAuthenticated();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWorkspaceId(Request $request) {
+    $workspace_id = $this->tempstore->get('active_workspace_id');
+    return $workspace_id ?: $this->defaultWorkspaceId;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persist(WorkspaceInterface $workspace) {
+    $this->tempstore->set('active_workspace_id', $workspace->id());
+    return parent::persist($workspace);
+  }
+
+}
diff --git a/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorBase.php b/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorBase.php
new file mode 100644
index 0000000000..6ce9e0eaaf
--- /dev/null
+++ b/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorBase.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\workspace\Negotiator;
+
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Class WorkspaceNegotiatorBase
+ */
+abstract class WorkspaceNegotiatorBase implements WorkspaceNegotiatorInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persist(WorkspaceInterface $workspace) {
+    return TRUE;
+  }
+
+}
diff --git a/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorInterface.php b/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorInterface.php
new file mode 100644
index 0000000000..1d5d1db074
--- /dev/null
+++ b/core/modules/workspace/src/Negotiator/WorkspaceNegotiatorInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\workspace\Negotiator;
+
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Interface WorkspaceNegotiatorInterface
+ */
+interface WorkspaceNegotiatorInterface {
+
+  /**
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   * @return bool
+   */
+  public function applies(Request $request);
+
+  /**
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   * @return string
+   */
+  public function getWorkspaceId(Request $request);
+
+  /**
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   * @return bool
+   */
+  public function persist(WorkspaceInterface $workspace);
+
+}
diff --git a/core/modules/workspace/src/ParamConverter/EntityRevisionConverter.php b/core/modules/workspace/src/ParamConverter/EntityRevisionConverter.php
new file mode 100644
index 0000000000..25f9d79097
--- /dev/null
+++ b/core/modules/workspace/src/ParamConverter/EntityRevisionConverter.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\workspace\ParamConverter;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\ParamConverter\ParamConverterInterface;
+use Drupal\Core\ParamConverter\ParamNotConvertedException;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Parameter converter for upcasting entity revision IDs to full objects.
+ */
+class EntityRevisionConverter implements ParamConverterInterface {
+
+  /**
+   * Entity manager which performs the upcasting in the end.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a new EntityRevisionConverter.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->entityManager = $entity_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function convert($value, $definition, $name, array $defaults) {
+    $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
+    if ($storage = $this->entityManager->getStorage($entity_type_id)) {
+      $entity = $storage->loadRevision($value);
+      // If the entity type is translatable, ensure we return the proper
+      // translation object for the current context.
+      if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
+        $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+      }
+      return $entity;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies($definition, $name, Route $route) {
+    if (!empty($definition['type']) && strpos($definition['type'], 'entity_revision:') === 0) {
+      $entity_type_id = substr($definition['type'], strlen('entity:'));
+      if (strpos($definition['type'], '{') !== FALSE) {
+        $entity_type_slug = substr($entity_type_id, 1, -1);
+        return $name != $entity_type_slug && in_array($entity_type_slug, $route->compile()->getVariables(), TRUE);
+      }
+      return $this->entityManager->hasDefinition($entity_type_id);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Determines the entity type ID given a route definition and route defaults.
+   *
+   * @param mixed $definition
+   *   The parameter definition provided in the route options.
+   * @param string $name
+   *   The name of the parameter.
+   * @param array $defaults
+   *   The route defaults array.
+   *
+   * @throws \Drupal\Core\ParamConverter\ParamNotConvertedException
+   *   Thrown when the dynamic entity type is not found in the route defaults.
+   *
+   * @return string
+   *   The entity type ID.
+   */
+  protected function getEntityTypeFromDefaults($definition, $name, array $defaults) {
+    $entity_type_id = substr($definition['type'], strlen('entity_revision:'));
+
+    // If the entity type is dynamic, it will be pulled from the route defaults.
+    if (strpos($entity_type_id, '{') === 0) {
+      $entity_type_slug = substr($entity_type_id, 1, -1);
+      if (!isset($defaults[$entity_type_slug])) {
+        throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted because the "%s" parameter is missing', $name, $entity_type_slug));
+      }
+      $entity_type_id = $defaults[$entity_type_slug];
+    }
+    return $entity_type_id;
+  }
+
+}
diff --git a/core/modules/workspace/src/Plugin/Block/WorkspaceBlock.php b/core/modules/workspace/src/Plugin/Block/WorkspaceBlock.php
new file mode 100644
index 0000000000..8b87f5aca4
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Block/WorkspaceBlock.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\workspace\WorkspaceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Block(
+ *   id = "workspace_switcher_block",
+ *   admin_label = @Translation("Workspace switcher"),
+ *   category = @Translation("Workspace"),
+ * )
+ */
+class WorkspaceBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * @param array $configuration
+   * @param string $plugin_id
+   * @param mixed $plugin_definition
+   * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, WorkspaceManagerInterface $workspace_manager, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->workspaceManager = $workspace_manager;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('workspace.manager'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    $build = [
+      // @todo the block depending on the toolbar is obscure; find a better way to generate this form
+      '#pre_render' => ['workspace_switcher_toolbar_pre_render'],
+      // This wil get filled in via pre-render.
+      'workspace_forms' => [],
+      '#attached' => [
+        'library' => [
+          'workspace/drupal.workspace.switcher',
+        ],
+      ],
+      '#cache' => [
+        'contexts' => $this->entityTypeManager->getDefinition('workspace')->getListCacheContexts(),
+        'tags' => $this->entityTypeManager->getDefinition('workspace')->getListCacheTags(),
+      ],
+    ];
+    return $build;
+  }
+
+}
diff --git a/core/modules/workspace/src/Plugin/Derivative/Workspace.php b/core/modules/workspace/src/Plugin/Derivative/Workspace.php
new file mode 100644
index 0000000000..e4dcb60f65
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Derivative/Workspace.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Derive an upstream plugin for each workspace.
+ */
+class Workspace extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $workspaceStorage;
+
+  /**
+   * Workspace constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $workspace_storage
+   */
+  public function __construct(EntityStorageInterface $workspace_storage) {
+    $this->workspaceStorage = $workspace_storage;
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity_type.manager')->getStorage('workspace')
+    );
+  }
+
+  /**
+   * Add a workspace plugin per workspace, with an ID in the format
+   * 'workspace:{id}', for example 'workspace:live'.
+   *
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $workspaces = $this->workspaceStorage->loadMultiple();
+    foreach ($workspaces as $workspace) {
+      $this->derivatives[$workspace->id()] = $base_plugin_definition;
+      $this->derivatives[$workspace->id()]['id'] = $base_plugin_definition['id'] . PluginBase::DERIVATIVE_SEPARATOR . $workspace->id();
+    }
+    return $this->derivatives;
+  }
+
+}
diff --git a/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItem.php b/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItem.php
new file mode 100644
index 0000000000..4c96eccb99
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItem.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * @FieldType(
+ *   id = "replication_history",
+ *   label = @Translation("Replication history"),
+ *   description = @Translation("History information for a replication."),
+ *   list_class = "\Drupal\workspace\Plugin\Field\FieldType\ReplicationHistoryItemList",
+ *   no_ui = TRUE
+ * )
+ */
+class ReplicationHistoryItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function mainPropertyName() {
+    return 'session_id';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['doc_write_failures'] = DataDefinition::create('integer')
+      ->setLabel(t('Write failures'))
+      ->setDescription(t('Number of failed document writes'))
+      ->setRequired(FALSE);
+
+    $properties['docs_read'] = DataDefinition::create('integer')
+      ->setLabel(t('Documents read'))
+      ->setDescription(t('Number of documents read.'))
+      ->setRequired(FALSE);
+
+    $properties['docs_written'] = DataDefinition::create('integer')
+      ->setLabel(t('Documents written'))
+      ->setDescription(t('Number of documents written.'))
+      ->setRequired(FALSE);
+
+    $properties['end_last_seq'] = DataDefinition::create('integer')
+      ->setLabel(t('End sequence'))
+      ->setDescription(t('Sequence ID where the replication ended.'))
+      ->setRequired(FALSE);
+
+    $properties['end_time'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('End time'))
+      ->setDescription(t('Date and time when replication ended.'))
+      ->setRequired(FALSE);
+
+    $properties['missing_checked'] = DataDefinition::create('integer')
+      ->setLabel(t('Missing checked'))
+      ->setDescription(t('Number of missing documents checked.'))
+      ->setRequired(FALSE);
+
+    $properties['missing_found'] = DataDefinition::create('integer')
+      ->setLabel(t('Missing found'))
+      ->setDescription(t('Number of missing documents found.'))
+      ->setRequired(FALSE);
+
+    $properties['recorded_seq'] = DataDefinition::create('integer')
+      ->setLabel(t('Recorded sequence'))
+      ->setDescription(t('Recorded intermediate sequence.'))
+      ->setRequired(FALSE);
+
+    $properties['session_id'] = DataDefinition::create('string')
+      ->setLabel(t('Session ID'))
+      ->setDescription(t('Unique session ID for the replication.'))
+      ->setRequired(TRUE);
+
+    $properties['start_last_seq'] = DataDefinition::create('integer')
+      ->setLabel(t('Start sequence'))
+      ->setDescription(t('Sequence ID where the replication started.'))
+      ->setRequired(FALSE);
+
+    $properties['start_time'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('Start time'))
+      ->setDescription(t('Date and time when replication started.'))
+      ->setRequired(FALSE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'columns' => [
+        'doc_write_failures' => [
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => FALSE,
+        ],
+        'docs_read' => [
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => FALSE,
+        ],
+        'docs_written' => [
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => FALSE,
+        ],
+        'end_last_seq' => [
+          'type' => 'int',
+          'size' => 'big',
+          'not null' => FALSE,
+        ],
+        'end_time' => [
+          'type' => 'varchar',
+          'length' => 50,
+          'not null' => FALSE,
+        ],
+        'missing_checked' => [
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => FALSE,
+        ],
+        'missing_found' => [
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => FALSE,
+        ],
+        'recorded_seq' => [
+          'type' => 'int',
+          'size' => 'big',
+          'not null' => FALSE,
+          'default' => 0,
+        ],
+        'session_id' => [
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => TRUE,
+        ],
+        'start_last_seq' => [
+          'type' => 'int',
+          'size' => 'big',
+          'not null' => FALSE,
+        ],
+        'start_time' => [
+          'type' => 'varchar',
+          'length' => 50,
+          'not null' => FALSE,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItemList.php b/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItemList.php
new file mode 100644
index 0000000000..6d7985b153
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Field/FieldType/ReplicationHistoryItemList.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemList;
+
+class ReplicationHistoryItemList extends FieldItemList {}
diff --git a/core/modules/workspace/src/Plugin/Field/FieldType/WorkspaceReferenceItem.php b/core/modules/workspace/src/Plugin/Field/FieldType/WorkspaceReferenceItem.php
new file mode 100644
index 0000000000..7e0b5b37f4
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Field/FieldType/WorkspaceReferenceItem.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * @FieldType(
+ *   id = "workspace_reference",
+ *   label = @Translation("Workspace reference"),
+ *   description = @Translation("This field stores a reference to the workspace the entity belongs to."),
+ *   no_ui = TRUE
+ * )
+ */
+class WorkspaceReferenceItem extends EntityReferenceItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultStorageSettings() {
+    return [
+      'target_type' => 'workspace',
+    ] + parent::defaultStorageSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties = parent::propertyDefinitions($field_definition);
+    $properties['status'] = DataDefinition::create('boolean')
+      ->setLabel(t('Status'))
+      ->setRequired(TRUE);
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    $schema = parent::schema($field_definition);
+    $schema['columns']['status'] = ['type' => 'int', 'size' => 'tiny'];
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyDefaultValue($notify = TRUE) {
+    $workspace_id = \Drupal::service('workspace.manager')->getActiveWorkspace();
+    $entity = $this->getEntity();
+    $this->setValue(['target_id' => $workspace_id, 'status' => $entity->isPublished()], $notify);
+    return $this;
+  }
+
+}
diff --git a/core/modules/workspace/src/Plugin/Upstream/Workspace.php b/core/modules/workspace/src/Plugin/Upstream/Workspace.php
new file mode 100644
index 0000000000..5fe2b8ecc2
--- /dev/null
+++ b/core/modules/workspace/src/Plugin/Upstream/Workspace.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\workspace\Plugin\Upstream;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\workspace\UpstreamInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a 'Workspace' upstream plugin to allow a workspace to be set as a
+ * source and / or target for content replication.
+ *
+ * @Upstream(
+ *   id = "workspace",
+ *   deriver = "Drupal\workspace\Plugin\Derivative\Workspace"
+ * )
+ */
+class Workspace extends PluginBase implements UpstreamInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * @var \Drupal\workspace\Entity\Workspace
+   */
+  protected $workspace;
+
+  /**
+   * Workspace constructor.
+   *
+   * @param array $configuration
+   * @param $plugin_id
+   * @param $plugin_definition
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->workspace = $entity_type_manager->getStorage('workspace')->load($this->getDerivativeId());
+  }
+
+  /**
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   * @param array $configuration
+   * @param string $plugin_id
+   * @param mixed $plugin_definition
+   *
+   * @return static
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public function getLabel() {
+    return $this->workspace->label();
+  }
+
+}
diff --git a/core/modules/workspace/src/Replication/DefaultReplicator.php b/core/modules/workspace/src/Replication/DefaultReplicator.php
new file mode 100644
index 0000000000..ed44738b51
--- /dev/null
+++ b/core/modules/workspace/src/Replication/DefaultReplicator.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\workspace\Replication;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\workspace\Changes\ChangesFactoryInterface;
+use Drupal\workspace\Entity\ReplicationLog;
+use Drupal\workspace\Entity\Workspace;
+use Drupal\workspace\Index\SequenceIndexInterface;
+use Drupal\workspace\UpstreamInterface;
+use Drupal\workspace\WorkspaceManagerInterface;
+
+/**
+ * The default replicator service, replicating from one workspace to another on
+ * a single site.
+ */
+class DefaultReplicator implements ReplicationInterface {
+
+  /**
+   * The workspace manager.
+   *
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * The changes factory.
+   *
+   * @var \Drupal\workspace\Changes\ChangesFactoryInterface
+   */
+  protected $changesFactory;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The sequence index.
+   *
+   * @var \Drupal\workspace\Index\SequenceIndexInterface
+   */
+  protected $sequenceIndex;
+
+  /**
+   * DefaultReplication constructor.
+   *
+   * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+   * @param \Drupal\workspace\Changes\ChangesFactoryInterface $changes_factory
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\workspace\Index\SequenceIndexInterface $sequence_index
+   */
+  public function __construct(WorkspaceManagerInterface $workspace_manager, ChangesFactoryInterface $changes_factory, EntityTypeManagerInterface $entity_type_manager, SequenceIndexInterface $sequence_index) {
+    $this->workspaceManager = $workspace_manager;
+    $this->changesFactory = $changes_factory;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->sequenceIndex = $sequence_index;
+  }
+
+  /**
+   * Only use this replicator if the source and target are workspaces. The
+   * Upstream plugin ID would be something like 'workspace:live' for the live
+   * workspace.
+   *
+   * {@inheritdoc}
+   */
+  public function applies(UpstreamInterface $source, UpstreamInterface $target) {
+    list($source_plugin, $source_id) = explode(':', $source->getPluginId());
+    list($target_plugin, $target_id) = explode(':', $target->getPluginId());
+    if ($source_plugin == 'workspace' && $target_plugin == 'workspace'
+    && !empty($source_id) && !empty($target_id)) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Replicating content from one workspace to another on the same site roughly
+   * following the same protocol as CouchDB replication
+   * (http://docs.couchdb.org/en/2.1.0/replication/protocol.html).
+   *
+   * {@inheritdoc}
+   */
+  public function replicate(UpstreamInterface $source, UpstreamInterface $target) {
+    list($source_plugin, $source_id) = explode(':', $source->getPluginId());
+    list($target_plugin, $target_id) = explode(':', $target->getPluginId());
+    $source = Workspace::load($source_id);
+    $target = Workspace::load($target_id);
+    $replication_id = \md5($source->id() . $target->id());
+    $start_time = new \DateTime();
+    $sessionId = \md5((\microtime(TRUE) * 1000000));
+    $replication_log = ReplicationLog::loadOrCreate($replication_id);
+    $current_active = $this->workspaceManager->getActiveWorkspace(TRUE);
+
+    // Set the source as the active workspace.
+    $this->workspaceManager->setActiveWorkspace($source);
+
+    // Get changes for the current workspace.
+    $history = $replication_log->getHistory();
+    $last_seq = isset($history[0]['recorded_seq']) ? $history[0]['recorded_seq'] : 0;
+    $changes = $this->changesFactory->get($source)->lastSeq($last_seq)->getNormal();
+    $rev_diffs = [];
+    foreach ($changes as $change) {
+      foreach ($change['changes'] as $change_item) {
+        $rev_diffs[$change['type']][] = $change_item['rev'];
+      }
+    }
+
+    // Get revision diff between source and target
+    $content_workspace_ids = [];
+    foreach ($rev_diffs as $entity_type_id => $revs) {
+      $content_workspace_ids[$entity_type_id] = $this->entityTypeManager
+        ->getStorage('content_workspace')
+        ->getQuery()
+        ->allRevisions()
+        ->condition('content_entity_type_id', $entity_type_id)
+        ->condition('content_entity_revision_id', $revs, 'IN')
+        ->condition('workspace', $target->id())
+        ->execute();
+    }
+    foreach ($content_workspace_ids as $entity_type_id => $ids) {
+      foreach ($ids as $id) {
+        $key = array_search($id, $rev_diffs[$entity_type_id]);
+        if (isset($key)) {
+          unset($rev_diffs[$entity_type_id][$key]);
+        }
+      }
+    }
+
+    $entities = [];
+    // Load each missing revision.
+    foreach ($rev_diffs as $entity_type_id => $revs) {
+      foreach ($revs as $rev) {
+        /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+        $entity = $this->entityTypeManager
+          ->getStorage($entity_type_id)
+          ->loadRevision($rev);
+        $entity->isDefaultRevision(TRUE);
+        $entities[] = $entity;
+      }
+    }
+
+    // Before saving set the active workspace to the target.
+    $this->workspaceManager->setActiveWorkspace($target);
+
+    // Save each revision on the target workspace
+    foreach ($entities as $entity) {
+      $entity->save();
+    }
+
+    // Log
+    $this->workspaceManager->setActiveWorkspace($current_active);
+
+    $replication_log->setHistory([
+      'recorded_seq' => $this->sequenceIndex->useWorkspace($source->id())->getLastSequenceId(),
+      'start_time' => $start_time->format('D, d M Y H:i:s e'),
+      'session_id' => $sessionId,
+    ]);
+    $replication_log->save();
+    return $replication_log;
+  }
+
+}
diff --git a/core/modules/workspace/src/Replication/ReplicationInterface.php b/core/modules/workspace/src/Replication/ReplicationInterface.php
new file mode 100644
index 0000000000..d08a4f6958
--- /dev/null
+++ b/core/modules/workspace/src/Replication/ReplicationInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\workspace\Replication;
+
+use Drupal\workspace\UpstreamInterface;
+
+/**
+ * Interface ReplicationInterface
+ */
+interface ReplicationInterface {
+
+  /**
+   * @param \Drupal\workspace\UpstreamInterface $source
+   * @param \Drupal\workspace\UpstreamInterface $target
+   *
+   * @return bool
+   */
+  public function applies(UpstreamInterface $source, UpstreamInterface $target);
+
+  /**
+   * @param \Drupal\workspace\UpstreamInterface $source
+   * @param \Drupal\workspace\UpstreamInterface $target
+   *
+   * @return \Drupal\workspace\Entity\ReplicationLogInterface
+   */
+  public function replicate(UpstreamInterface $source, UpstreamInterface $target);
+
+}
diff --git a/core/modules/workspace/src/Replication/ReplicationManager.php b/core/modules/workspace/src/Replication/ReplicationManager.php
new file mode 100644
index 0000000000..20014e6e2b
--- /dev/null
+++ b/core/modules/workspace/src/Replication/ReplicationManager.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\workspace\Replication;
+
+use Drupal\workspace\UpstreamInterface;
+
+/**
+ * Manage the replication tagged services.
+ */
+class ReplicationManager {
+
+  /**
+   * @var \Drupal\workspace\Replication\ReplicationInterface[]
+   */
+  protected $replicators = [];
+
+  /**
+   * @param \Drupal\workspace\Replication\ReplicationInterface $replicator
+   */
+  public function addReplicator(ReplicationInterface $replicator, $priority) {
+    $this->replicators[$priority][] = $replicator;
+  }
+
+  /**
+   * Find all replicators that apply for the source and target upstream plugins
+   * and run the replication for each replicator.
+   *
+   * @param \Drupal\workspace\UpstreamInterface $source
+   * @param \Drupal\workspace\UpstreamInterface $target
+   *
+   * @return \Drupal\workspace\Entity\ReplicationLogInterface
+   */
+  public function replicate(UpstreamInterface $source, UpstreamInterface $target) {
+    foreach ($this->replicators as $replicators) {
+      /** @var \Drupal\workspace\Replication\ReplicationInterface $replicator */
+      foreach ($replicators as $replicator) {
+        if ($replicator->applies($source, $target)) {
+          return $replicator->replicate($source, $target);
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/workspace/src/UpstreamInterface.php b/core/modules/workspace/src/UpstreamInterface.php
new file mode 100644
index 0000000000..4b51d51666
--- /dev/null
+++ b/core/modules/workspace/src/UpstreamInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * An upstream is a source or a target for replication. For example it could be
+ * a workspace, a remote site, a non-drupal application or a database such as
+ * CouchDB.
+ *
+ * When a replication happens the replicator will determine if it should run
+ * based on the source and target upstream plugins. Then the replication will
+ * use data from the upstream plugins to perform the replication. For example an
+ * internal replication might just need the workspace IDs, but a contrib module
+ * performing an external replication may need host name, port, username,
+ * password etc.
+ */
+interface UpstreamInterface extends PluginInspectionInterface {
+
+  /**
+   * Returns the label of the upstream. This is used as a form label where a user
+   * selects the target for replicating to.
+   *
+   * @return string
+   *   The label of the upstream.
+   */
+  public function getLabel();
+
+}
diff --git a/core/modules/workspace/src/UpstreamManager.php b/core/modules/workspace/src/UpstreamManager.php
new file mode 100644
index 0000000000..201a6557f8
--- /dev/null
+++ b/core/modules/workspace/src/UpstreamManager.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Class UpstreamManager
+ */
+class UpstreamManager extends DefaultPluginManager {
+
+  /**
+   * UpstreamManager constructor.
+   *
+   * @param \Traversable $namespaces
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/Upstream', $namespaces, $module_handler, 'Drupal\workspace\UpstreamInterface', 'Drupal\workspace\Annotation\Upstream');
+    $this->alterInfo('workspace_upstream_info');
+    $this->setCacheBackend($cache_backend, 'workspace_upstream');
+  }
+
+}
diff --git a/core/modules/workspace/src/WorkspaceAccessControlHandler.php b/core/modules/workspace/src/WorkspaceAccessControlHandler.php
new file mode 100644
index 0000000000..cd01bf02e1
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceAccessControlHandler.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the workspace entity type.
+ *
+ * @see \Drupal\workspace\Entity\Workspace
+ */
+class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $entity */
+    $operations = [
+      'view' => ['any' => 'view any workspace', 'own' => 'view own workspace'],
+      'update' => ['any' => 'edit any workspace', 'own' => 'edit own workspace'],
+      'delete' => ['any' => 'delete any workspace', 'own' => 'delete own workspace'],
+    ];
+
+    // The default workspace is always viewable, no matter what.
+    $default_workspace = \Drupal::getContainer()->getParameter('workspace.default');
+    $result = AccessResult::allowedIf($operation == 'view' && $entity->id() == $default_workspace)->addCacheableDependency($entity)
+      // Or if the user has permission to access any workspace at all.
+      ->orIf(AccessResult::allowedIfHasPermission($account, $operations[$operation]['any']))
+      // Or if it's their own workspace, and they have permission to access their own workspace.
+      ->orIf(
+        AccessResult::allowedIf($entity->getOwnerId() == $account->id())->addCacheableDependency($entity)
+          ->andIf(AccessResult::allowedIfHasPermission($account, $operations[$operation]['own']))
+      )
+      ->orIf(AccessResult::allowedIfHasPermission($account, $operation . '_workspace_' . $entity->id()));
+
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'create workspace');
+  }
+
+}
diff --git a/core/modules/workspace/src/WorkspaceAccessException.php b/core/modules/workspace/src/WorkspaceAccessException.php
new file mode 100644
index 0000000000..55b58e6a2f
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceAccessException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Drupal\workspace;
+
+
+use Drupal\Core\Access\AccessException;
+
+/**
+ * Exception thrown when trying to switch to an inaccessible workspace.
+ */
+class WorkspaceAccessException extends AccessException {
+
+}
diff --git a/core/modules/workspace/src/WorkspaceCacheContext.php b/core/modules/workspace/src/WorkspaceCacheContext.php
new file mode 100644
index 0000000000..bc44b412c4
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceCacheContext.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\Context\CacheContextInterface;
+
+/**
+ * Defines the WorkspaceCacheContext service, for "per workspace" caching.
+ *
+ * Cache context ID: 'workspace'.
+ */
+class WorkspaceCacheContext implements CacheContextInterface {
+
+  /**
+   * The workspace manager.
+   *
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * Constructs a new WorkspaceCacheContext service.
+   *
+   * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+   *   The workspace manager.
+   */
+  public function __construct(WorkspaceManagerInterface $workspace_manager) {
+    $this->workspaceManager = $workspace_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Workspace');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return 'ws.' . $this->workspaceManager->getActiveWorkspace();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata($type = NULL) {
+    return new CacheableMetadata();
+  }
+
+}
diff --git a/core/modules/workspace/src/WorkspaceListBuilder.php b/core/modules/workspace/src/WorkspaceListBuilder.php
new file mode 100644
index 0000000000..d16e238b8d
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceListBuilder.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a class to build a listing of workspace entities.
+ *
+ * @see \Drupal\workspace\Entity\Workspace
+ */
+class WorkspaceListBuilder extends EntityListBuilder {
+
+  /**
+   * The workspace manager service.
+   *
+   * @var \Drupal\workspace\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * Constructs a new EntityListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+   *   The workspace manager service.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, WorkspaceManagerInterface $workspace_manager) {
+    parent::__construct($entity_type, $storage);
+    $this->workspaceManager = $workspace_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('workspace.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['label'] = $this->t('Workspace');
+    $header['uid'] = $this->t('Owner');
+    $header['status'] = $this->t('Status');
+
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $entity */
+    $row['label'] = $entity->label() . ' (' . $entity->id() . ')';
+    $row['owner'] = $entity->getOwner()->getDisplayname();
+    $active_workspace = $this->workspaceManager->getActiveWorkspace();
+    $row['status'] = $active_workspace == $entity->id() ? $this->t('Active') : $this->t('Inactive');
+
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOperations(EntityInterface $entity) {
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $entity */
+    $operations = parent::getDefaultOperations($entity);
+    if (isset($operations['edit'])) {
+      $operations['edit']['query']['destination'] = $entity->toUrl('collection');
+    }
+
+    $active_workspace = $this->workspaceManager->getActiveWorkspace();
+    if ($entity->id() != $active_workspace) {
+      $operations['activate'] = [
+        'title' => $this->t('Set Active'),
+        'weight' => 20,
+        'url' => $entity->toUrl('activate-form', ['query' => ['destination' => $entity->toUrl('collection')]]),
+      ];
+    }
+
+    if ('workspace:' . $entity->id() != $entity->get('upstream')->value) {
+      $operations['deployment'] = [
+        'title' => $this->t('Deploy content'),
+        'weight' => 20,
+        'url' => $entity->toUrl('deployment-form', ['query' => ['destination' => $entity->toUrl('collection')]]),
+      ];
+    }
+
+    return $operations;
+  }
+
+}
diff --git a/core/modules/workspace/src/WorkspaceManager.php b/core/modules/workspace/src/WorkspaceManager.php
new file mode 100644
index 0000000000..ecfc336a09
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceManager.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Drupal\workspace\Negotiator\WorkspaceNegotiatorInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides the workspace manager.
+ */
+class WorkspaceManager implements WorkspaceManagerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * @var string[]
+   */
+  protected $blacklist = [
+    'content_workspace',
+    'workspace'
+  ];
+
+  /**
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * @var array
+   */
+  protected $negotiators = [];
+
+  /**
+   * @var array
+   */
+  protected $sortedNegotiators;
+
+  /**
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+   * @param \Psr\Log\LoggerInterface $logger
+   */
+  public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, LoggerInterface $logger = NULL) {
+    $this->requestStack = $request_stack;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->currentUser = $current_user;
+    $this->logger = $logger ?: new NullLogger();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function entityCanBelongToWorkspaces(EntityInterface $entity) {
+    return $this->entityTypeCanBelongToWorkspaces($entity->getEntityType());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function entityTypeCanBelongToWorkspaces(EntityTypeInterface $entity_type) {
+    if (!in_array($entity_type->id(), $this->blacklist, TRUE)
+      && is_a($entity_type->getClass(), EntityPublishedInterface::class, TRUE)
+      && $entity_type->isRevisionable()) {
+      return TRUE;
+    }
+    $this->blacklist[] = $entity_type->id();
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSupportedEntityTypes() {
+    $entity_types = [];
+    foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
+      if ($this->entityTypeCanBelongToWorkspaces($entity_type)) {
+        $entity_types[$entity_type_id] = $entity_type;
+      }
+    }
+    return $entity_types;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addNegotiator(WorkspaceNegotiatorInterface $negotiator, $priority) {
+    $this->negotiators[$priority][] = $negotiator;
+    $this->sortedNegotiators = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo {@link https://www.drupal.org/node/2600382 Access check.}
+   */
+  public function getActiveWorkspace($object = FALSE) {
+    $request = $this->requestStack->getCurrentRequest();
+    foreach ($this->getSortedNegotiators() as $negotiator) {
+      if ($negotiator->applies($request)) {
+        if ($workspace_id = $negotiator->getWorkspaceId($request)) {
+          if ($object) {
+            return $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+          }
+          else {
+            return $workspace_id;
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setActiveWorkspace(WorkspaceInterface $workspace) {
+    $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default');
+    // If the current user doesn't have access to view the workspace, they
+    // shouldn't be allowed to switch to it.
+    // @todo Could this be handled better?
+    if (!$workspace->access('view') && ($workspace->id() != $default_workspace_id)) {
+      $this->logger->error('Denied access to view workspace {workspace}', ['workspace' => $workspace->label()]);
+      throw new WorkspaceAccessException('The user does not have permission to view that workspace.');
+    }
+
+    // Set the workspace on the proper negotiator.
+    $request = $this->requestStack->getCurrentRequest();
+    foreach ($this->getSortedNegotiators() as $negotiator) {
+      if ($negotiator->applies($request)) {
+        $negotiator->persist($workspace);
+        break;
+      }
+    }
+
+    $supported_entity_types = $this->getSupportedEntityTypes();
+    foreach ($supported_entity_types as $supported_entity_type) {
+      $this->entityTypeManager->getStorage($supported_entity_type->id())->resetCache();
+    }
+
+    return $this;
+  }
+
+  /**
+   * @return \Drupal\workspace\Negotiator\WorkspaceNegotiatorInterface[]
+   */
+  protected function getSortedNegotiators() {
+    if (!isset($this->sortedNegotiators)) {
+      // Sort the negotiators according to priority.
+      krsort($this->negotiators);
+      // Merge nested negotiators from $this->negotiators into
+      // $this->sortedNegotiators.
+      $this->sortedNegotiators = [];
+      foreach ($this->negotiators as $builders) {
+        $this->sortedNegotiators = array_merge($this->sortedNegotiators, $builders);
+      }
+    }
+    return $this->sortedNegotiators;
+  }
+
+}
diff --git a/core/modules/workspace/src/WorkspaceManagerInterface.php b/core/modules/workspace/src/WorkspaceManagerInterface.php
new file mode 100644
index 0000000000..35ea690eb2
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceManagerInterface.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\workspace\Entity\WorkspaceInterface;
+use Drupal\workspace\Negotiator\WorkspaceNegotiatorInterface;
+
+/**
+ * Interface WorkspaceManagerInterface
+ */
+interface WorkspaceManagerInterface {
+
+  /**
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *
+   * @return bool
+   */
+  public function entityCanBelongToWorkspaces(EntityInterface $entity);
+
+  /**
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *
+   * @return bool
+   */
+  public function entityTypeCanBelongToWorkspaces(EntityTypeInterface $entity_type);
+
+  /**
+   * @return \Drupal\Core\Entity\EntityTypeInterface[]
+   */
+  public function getSupportedEntityTypes();
+
+  /**
+   * @param \Drupal\workspace\Negotiator\WorkspaceNegotiatorInterface $negotiator
+   * @param int $priority
+   */
+  public function addNegotiator(WorkspaceNegotiatorInterface $negotiator, $priority);
+
+  /**
+   * @param bool $object
+   *   Should the active workspace be returned as an object.
+   *
+   * @return \Drupal\workspace\Entity\WorkspaceInterface | int
+   */
+  public function getActiveWorkspace($object = FALSE);
+
+  /**
+   * Sets the active workspace for the site/session.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace to set as active.
+   *
+   * @return \Drupal\workspace\WorkspaceManagerInterface
+   *
+   * @throws WorkspaceAccessException
+   */
+  public function setActiveWorkspace(WorkspaceInterface $workspace);
+
+}
diff --git a/core/modules/workspace/src/WorkspaceServiceProvider.php b/core/modules/workspace/src/WorkspaceServiceProvider.php
new file mode 100644
index 0000000000..7b82aeb030
--- /dev/null
+++ b/core/modules/workspace/src/WorkspaceServiceProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceProviderBase;
+
+/**
+ * Defines a service profiler for the workspace module.
+ */
+class WorkspaceServiceProvider extends ServiceProviderBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container) {
+    $renderer_config = $container->getParameter('renderer.config');
+    $renderer_config['required_cache_contexts'][] = 'workspace';
+    $container->setParameter('renderer.config', $renderer_config);
+  }
+
+}
diff --git a/core/modules/workspace/templates/workspace-toolbox.html.twig b/core/modules/workspace/templates/workspace-toolbox.html.twig
new file mode 100644
index 0000000000..24675a886d
--- /dev/null
+++ b/core/modules/workspace/templates/workspace-toolbox.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation for the Workspace toolbox.
+ *
+ * @see template_preprocess_workspace_toolbox()
+ *
+ * @ingroup themeable
+ */
+#}
+
+<div class="toolbox">
+  <ul>
+    {% for workspace_form in workspace_forms %}
+      <li>{{ workspace_form }}</li>
+    {% endfor %}
+  </ul>
+
+  <div class="control-block">
+    {{ control_block }}
+    {{ deploy_form }}
+  </div>
+  <button type="button" class="toolbox-close" aria-pressed="false">Close Workspace toolbox</button>
+</div>
\ No newline at end of file
diff --git a/core/modules/workspace/tests/src/Functional/ExistingContentTest.php b/core/modules/workspace/tests/src/Functional/ExistingContentTest.php
new file mode 100644
index 0000000000..e0be9b443a
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/ExistingContentTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\workspace\Entity\Workspace;
+
+/**
+ * Tests using workspaces with existing content.
+ *
+ * @group workspace
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class ExistingContentTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+  use BlockCreationTrait {
+    placeBlock as drupalPlaceBlock;
+  }
+
+  public static $modules = ['node', 'user', 'block'];
+
+  protected $profile = 'standard';
+
+  /**
+   * Tests workspaces with existing nodes.
+   */
+  public function testExistingContent() {
+    $this->createNodeType('Test', 'test');
+
+    $this->drupalLogin($this->rootUser);
+
+    $published_node = $this->createNodeThroughUI('Published node', 'test', TRUE);
+    $unpublished_node = $this->createNodeThroughUI('Unpublished node', 'test', FALSE);
+
+    $this->drupalLogout();
+
+    \Drupal::service('module_installer')->install(['workspace']);
+    $this->rebuildContainer();
+    $this->drupalLogin($this->rootUser);
+    $live = Workspace::load('live');
+    $stage = Workspace::load('stage');
+    $this->setupWorkspaceSwitcherBlock();
+
+    $this->assertNull($published_node->workspace->target_id);
+    $this->assertNull($unpublished_node->workspace->target_id);
+
+    $this->switchToWorkspace($stage);
+    $session = $this->assertSession();
+    $session->pageTextContains('Published node');
+    $session->pageTextNotContains('Unpublished node');
+
+    $this->drupalGet('/node/' . $published_node->id() . '/edit');
+    $session->pageTextContains('Published node');
+    $this->drupalPostForm(NULL, [
+      'title[0][value]' => 'Published Stage node'
+    ], t('Save'));
+    $session->pageTextContains('Published stage node');
+
+    $this->drupalGet('/node/' . $unpublished_node->id() . '/edit');
+    $session->pageTextContains('Unpublished node');
+    $this->drupalPostForm(NULL, [
+      'title[0][value]' => 'Published Unpublished Stage node',
+      'status[value]' => TRUE,
+    ], t('Save'));
+    $session->pageTextContains('Published Unpublished stage node');
+
+    $this->drupalGet('<front>');
+    $session->pageTextContains('Published Stage node');
+    $session->pageTextContains('Published Unpublished stage node');
+
+    $this->switchToWorkspace($live);
+    $session->pageTextContains('Published node');
+    $session->pageTextNotContains('Unpublished node');
+
+    /** @var \Drupal\workspace\Replication\ReplicationManager $replicator */
+    $replicator = \Drupal::service('workspace.replication_manager');
+    /** @var \Drupal\workspace\UpstreamManager $upstream */
+    $upstream = \Drupal::service('workspace.upstream_manager');
+    $replicator->replicate(
+      $upstream->createInstance('workspace:' . $stage->id()),
+      $upstream->createInstance('workspace:' . $live->id())
+    );
+
+    $this->drupalGet('<front>');
+    $session->pageTextContains('Published Stage node');
+    $session->pageTextContains('Published Unpublished stage node');
+
+    $this->switchToWorkspace($stage);
+    $session->pageTextContains('Published Stage node');
+    $session->pageTextContains('Published Unpublished stage node');
+
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/ReplicationTest.php b/core/modules/workspace/tests/src/Functional/ReplicationTest.php
new file mode 100644
index 0000000000..fdf9e6f300
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/ReplicationTest.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Class ReplicationTest
+ *
+ * @group workspace
+ */
+class ReplicationTest extends BrowserTestBase {
+
+  use WorkspaceTestUtilities;
+
+  use BlockCreationTrait {
+    placeBlock as drupalPlaceBlock;
+  }
+
+  public static $modules = [
+    'system',
+    'node',
+    'user',
+    'block',
+    'workspace',
+    'taxonomy',
+    'entity_reference',
+    'field',
+    'field_ui',
+    'field_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->createNodeType('Test', 'test');
+
+    $permissions = [
+      'create workspace',
+      'view any workspace',
+      'create test content',
+      'edit own test content',
+      'access administration pages',
+      'administer taxonomy',
+      'administer menu',
+      'access content overview',
+      'administer content types',
+      'administer node display',
+      'administer node fields',
+      'administer node form display',
+      'administer workspaces',
+    ];
+    $test_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($test_user);
+
+    $this->setupWorkspaceSwitcherBlock();
+  }
+
+  public function testNodeReplication() {
+    $dev = $this->createWorkspaceThroughUI('Dev', 'dev');
+    $stage = $this->getOneWorkspaceByLabel('Stage');
+    $live = $this->getOneWorkspaceByLabel('Live');
+    $this->switchToWorkspace($dev);
+
+    $this->drupalGet('/node/add/test');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+    $page = $session->getPage();
+    $page->fillField('Title', 'Test node');
+    $page->findButton(t('Save'))->click();
+    $page = $session->getPage();
+    $this->assertTrue($page->hasContent("Test node has been created"));
+    $this->drupalGet('/node/1/edit');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+    $page->findButton(t('Save'))->click();
+
+    $this->assertEquals($dev->id(), $this->getOneEntityByLabel('node', 'Test node')->workspace->target_id);
+
+    $this->drupalGet('/admin/structure/workspace/' . $dev->id() . '/deployment');
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $this->assertEquals(200, $session->getStatusCode());
+    $this->assertTrue($page->hasContent('Update Dev from Stage or deploy to Stage.'));
+    $page->findButton('edit-deploy')->click();
+    $session->getPage()->hasContent('Successful deployment');
+
+    $this->switchToWorkspace($stage);
+    $this->assertEquals($stage->id(), $this->getOneEntityByLabel('node', 'Test node')->workspace->target_id);
+
+    $this->drupalGet('/node/add/test');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+    $page = $session->getPage();
+    $page->fillField('Title', 'Test stage node');
+    $page->findButton(t('Save'))->click();
+    $page = $session->getPage();
+    $page->hasContent("Test stage node has been created");
+
+    $this->assertEquals($stage->id(), $this->getOneEntityByLabel('node', 'Test stage node')->workspace->target_id);
+
+    $this->drupalGet('/admin/structure/workspace/' . $stage->id() . '/deployment');
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $this->assertEquals(200, $session->getStatusCode());
+    $this->assertTrue($page->hasContent('Update Stage from Live or deploy to Live.'));
+    $page->findButton('edit-deploy')->click();
+    $session->getPage()->hasContent('Successful deployment');
+
+    $this->switchToWorkspace($live);
+    $this->assertEquals($live->id(), $this->getOneEntityByLabel('node', 'Test node')->workspace->target_id);
+    $this->assertEquals($live->id(), $this->getOneEntityByLabel('node', 'Test stage node')->workspace->target_id);
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceBypassTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceBypassTest.php
new file mode 100644
index 0000000000..e2b0dd92a1
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceBypassTest.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests access bypass permission controls on workspaces.
+ *
+ * @group workspace
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class WorkspaceBypassTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+  use BlockCreationTrait {
+    placeBlock as drupalPlaceBlock;
+  }
+
+  public static $modules = ['node', 'user', 'block', 'workspace'];
+
+  /**
+   * Verifies that a user can edit anything in a workspace with a specific perm.
+   */
+  public function testBypassSpecificWorkspace() {
+    $permissions = [
+      'create workspace',
+      'edit own workspace',
+      'view own workspace',
+    ];
+
+    $this->createNodeType('Test', 'test');
+    $this->setupWorkspaceSwitcherBlock();
+
+    $ditka = $this->drupalCreateUser(array_merge($permissions, ['create test content']));
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($ditka);
+
+    $vanilla_node = $this->createNodeThroughUI('Vanilla node', 'test');
+    $this->assertEquals('live', $vanilla_node->workspace->target_id);
+
+    $bears = $this->createWorkspaceThroughUI('Bears', 'bears');
+    $this->switchToWorkspace($bears);
+
+    // Now create a node in the Bears workspace, as the owner of that workspace.
+    $ditka_bears_node = $this->createNodeThroughUI('Ditka Bears node', 'test');
+    $this->assertEquals($bears->id(), $ditka_bears_node->workspace->entity->id());
+    $ditka_bears_node_id = $ditka_bears_node->id();
+
+    // Create a new user that should be able to edit anything in the Bears workspace.
+    $lombardi = $this->drupalCreateUser(array_merge($permissions, ['view_workspace_' . $bears->id(), 'bypass_entity_access_workspace_' . $bears->id()]));
+    $this->drupalLogin($lombardi);
+    $this->switchToWorkspace($bears);
+
+    // Because Lombardi has the bypass permission, he should be able to
+    // create and edit any node.
+
+    $this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $lombardi_bears_node = $this->createNodeThroughUI('Lombardi Bears node', 'test');
+    $this->assertEquals($bears->id(), $lombardi_bears_node->workspace->entity->id());
+    $lombardi_bears_node_id = $lombardi_bears_node->id();
+
+    $this->drupalLogin($ditka);
+    $this->switchToWorkspace($bears);
+
+    $this->drupalGet('/node/' . $lombardi_bears_node_id . '/edit');
+    $session = $this->getSession();
+    $this->assertEquals(403, $session->getStatusCode());
+
+    // Create a new user that should NOT be able to edit anything in the Bears workspace.
+    $belichick = $this->drupalCreateUser(array_merge($permissions, ['view_workspace_' . $bears->id()]));
+    $this->drupalLogin($belichick);
+    $this->switchToWorkspace($bears);
+
+    $this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
+    $session = $this->getSession();
+    $this->assertEquals(403, $session->getStatusCode());
+
+  }
+
+  /**
+   * Verifies that a user can edit anything in a workspace they own.
+   */
+  public function testBypassOwnWorkspace() {
+    $permissions = [
+      'create workspace',
+      'edit own workspace',
+      'view own workspace',
+      'bypass entity access own workspace',
+    ];
+
+    $this->createNodeType('Test', 'test');
+    $this->setupWorkspaceSwitcherBlock();
+
+    $ditka = $this->drupalCreateUser(array_merge($permissions, ['create test content']));
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($ditka);
+
+    $vanilla_node = $this->createNodeThroughUI('Vanilla node', 'test');
+    $this->assertEquals('live', $vanilla_node->workspace->target_id);
+
+    $bears = $this->createWorkspaceThroughUI('Bears', 'bears');
+    $this->switchToWorkspace($bears);
+
+    // Now create a node in the Bears workspace, as the owner of that workspace.
+    $ditka_bears_node = $this->createNodeThroughUI('Ditka Bears node', 'test');
+    $this->assertEquals($bears->id(), $ditka_bears_node->workspace->entity->id());
+    $ditka_bears_node_id = $ditka_bears_node->id();
+
+    // Editing both nodes should be possible.
+
+    $this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+
+    // Create a new user that should be able to edit anything in the Bears workspace.
+    $lombardi = $this->drupalCreateUser(array_merge($permissions, ['view_workspace_' . $bears->id()]));
+    $this->drupalLogin($lombardi);
+    $this->switchToWorkspace($bears);
+
+    // Because editor 2 has the bypass permission, he should be able to
+    // create and edit any node.
+
+    $this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
+    $session = $this->getSession();
+    $this->assertEquals(403, $session->getStatusCode());
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceEntityTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceEntityTest.php
new file mode 100644
index 0000000000..09303f04ba
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceEntityTest.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests creating and loading entities in workspaces.
+ *
+ * @group workspace
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class WorkspaceEntityTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+  use BlockCreationTrait {
+    placeBlock as drupalPlaceBlock;
+  }
+
+  public static $modules = ['node', 'user', 'block', 'workspace'];
+
+  protected $profile = 'standard';
+
+  /**
+   * Tests creating and loading nodes.
+   *
+   * @dataProvider nodeEntityTestCases
+   */
+  public function testNodeEntity($initial_workspace) {
+    $permissions = [
+      'administer nodes',
+      'create workspace',
+      'edit any workspace',
+      'view any workspace',
+    ];
+
+    $this->createNodeType('Test', 'test');
+    $this->setupWorkspaceSwitcherBlock();
+
+    $buster = $this->drupalCreateUser(array_merge($permissions, ['view own unpublished content', 'create test content', 'edit own test content']));
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($buster);
+
+    $workspaces = [
+      'live' => $this->getOneWorkspaceByLabel('Live'),
+      'stage' => $this->getOneWorkspaceByLabel('Stage'),
+      'dev' => $this->createWorkspaceThroughUI('Dev', 'dev'),
+    ];
+    $default = \Drupal::getContainer()->getParameter('workspace.default');
+    $this->switchToWorkspace($workspaces[$initial_workspace]);
+
+    $workspace_manager = \Drupal::service('workspace.manager');
+    $this->assertEquals($initial_workspace, $workspace_manager->getActiveWorkspace());
+
+    $vanilla_node = $this->createNodeThroughUI('Vanilla node', 'test');
+    $this->assertEquals($initial_workspace, $vanilla_node->workspace->target_id);
+
+    $this->drupalGet('/node');
+    $this->assertSession()->pageTextContains('Vanilla node');
+    $this->drupalGet('/node/' . $vanilla_node->id());
+    $this->assertSession()->pageTextContains('Vanilla node');
+
+    $strawberry_node = $this->createNodeThroughUI('Strawberry node', 'test', FALSE);
+    $this->assertEquals($initial_workspace, $strawberry_node->workspace->target_id);
+
+    $this->drupalGet('/node');
+    $this->assertSession()->pageTextNotContains('Strawberry node');
+    $this->drupalGet('/node/' . $strawberry_node->id());
+    $this->assertSession()->pageTextContains('Strawberry node');
+
+    $chocolate_node = $this->createNodeThroughUI('Chocolate node', 'test', FALSE);
+    $this->assertEquals($initial_workspace, $chocolate_node->workspace->target_id);
+
+    $this->drupalGet('/node');
+    $this->assertSession()->pageTextNotContains('Chocolate node');
+    $this->drupalGet('/node/' . $chocolate_node->id());
+    $this->assertSession()->pageTextContains('Chocolate node');
+
+    $this->drupalPostForm('/node/' . $chocolate_node->id() . '/edit', [
+      'title[0][value]' => 'Mint node',
+      'status[value]' => TRUE,
+    ], t('Save'));
+
+    $this->drupalGet('/node');
+    $this->assertSession()->pageTextContains('Mint node');
+    $this->drupalGet('/node/' . $chocolate_node->id());
+    $this->assertSession()->pageTextContains('Mint node');
+
+    foreach ($workspaces as $workspace_id => $workspace) {
+      if ($workspace_id != $initial_workspace) {
+        $this->switchToWorkspace($workspace);
+
+        if ($initial_workspace == $default || $workspace_id == $default) {
+          // When the node started on the default workspace, or the current
+          // workspace is default, entity queries should return the correct
+          // revision.
+          $node_list = \Drupal::entityTypeManager()
+            ->getStorage('node')
+            ->loadByProperties(['title' => $vanilla_node->label()]);
+          $this->assertSame($vanilla_node->getRevisionId(), reset($node_list)->getRevisionId());
+        }
+        else {
+          // When the node was created on a non-default workspace and the
+          // current workspace is not the default, entity queries should return
+          // nothing.
+          $node_list = \Drupal::entityTypeManager()
+            ->getStorage('node')
+            ->loadByProperties(['title' => $vanilla_node->label()]);
+          $this->assertSame(FALSE, reset($node_list));
+        }
+
+        // Entity load and load_multiple should always return the default
+        // revision.
+        $node_load = \Drupal::entityTypeManager()
+          ->getStorage('node')
+          ->load($vanilla_node->id());
+        $this->assertSame($vanilla_node->getRevisionId(), $node_load->getRevisionId());
+        $node = \Drupal::entityTypeManager()
+          ->getStorage('node')
+          ->loadUnchanged($vanilla_node->id());
+        $this->assertSame($vanilla_node->getRevisionId(), $node->getRevisionId());
+
+        if ($initial_workspace == $default) {
+          // Then the node was created on the default workspace it should
+          // appear via the UI on all other workspaces.
+          $this->drupalGet('/node');
+          $this->assertSession()->statusCodeEquals(200);
+          $this->assertSession()->pageTextContains('Vanilla node');
+          $this->drupalGet('/node/' . $vanilla_node->id());
+          $this->assertSession()->statusCodeEquals(200);
+          $this->assertSession()->pageTextContains('Vanilla node');
+        }
+        else {
+          // When the node was not created on the default it should only not
+          // appear via the UI.
+          $this->drupalGet('/node');
+          $this->assertSession()->statusCodeEquals(200);
+          $this->assertSession()->pageTextNotContains('Vanilla node');
+          $this->drupalGet('/node/' . $vanilla_node->id());
+          $this->assertSession()->statusCodeEquals(403);
+          $this->assertSession()->pageTextNotContains('Vanilla node');
+        }
+      }
+    }
+  }
+
+  public function nodeEntityTestCases() {
+    return [
+      ['live'],
+      ['stage'],
+      ['dev'],
+    ];
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceIndividualPermissionsTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceIndividualPermissionsTest.php
new file mode 100644
index 0000000000..33f692c73f
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceIndividualPermissionsTest.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests permission controls on workspaces.
+ *
+ * @group workspace
+ *
+ * @runTestsInSeparateProcesses
+ *
+ * @preserveGlobalState disabled
+ */
+class WorkspaceIndividualPermissionsTest extends BrowserTestBase {
+
+  use WorkspaceTestUtilities;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['workspace', 'workspace'];
+
+  /**
+   * Verifies that a user can create and edit only their own workspace.
+   */
+  public function testEditIndividualWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+      'view own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    // Now login as a different user with permission to edit that workspace,
+    // specifically.
+
+    $editor2 = $this->drupalCreateUser(array_merge($permissions, ['update_workspace_' . $bears->id()]));
+
+    $this->drupalLogin($editor2);
+    $session = $this->getSession();
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+  }
+
+  /**
+   * Verifies that a user can view a specific workspace.
+   */
+  public function testViewIndividualWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    // Now login as a different user and create a workspace.
+
+    $editor2 = $this->drupalCreateUser(array_merge($permissions, ['view_workspace_' . $bears->id()]));
+
+    $this->drupalLogin($editor2);
+    $session = $this->getSession();
+
+    $this->createWorkspaceThroughUI('Packers', 'packers');
+
+    $packers = $this->getOneWorkspaceByLabel('Packers');
+
+    // Load the activate form for the Bears workspace. It should work, because
+    // the user has the permission specific to that workspace.
+    $this->drupalGet("admin/structure/workspace/{$bears->id()}/activate");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    // But editor 1 cannot view the Packers workspace.
+
+    $this->drupalLogin($editor1);
+    $this->drupalGet("admin/structure/workspace/{$packers->id()}/activate");
+    $this->assertEquals(403, $session->getStatusCode());
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspacePermissionsTest.php b/core/modules/workspace/tests/src/Functional/WorkspacePermissionsTest.php
new file mode 100644
index 0000000000..dce008d121
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspacePermissionsTest.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests permission controls on workspaces.
+ *
+ * @group workspace
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class WorkspacePermissionsTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+
+  /**
+   * @var array
+   */
+  public static $modules = ['workspace', 'workspace'];
+
+  /**
+   * Verifies that a user can create but not edit a workspace.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  public function testCreateWorkspace() {
+    $editor = $this->drupalCreateUser([
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+    ]);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor);
+    $session = $this->getSession();
+
+    $this->drupalGet('/admin/structure/workspace/add');
+
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $page = $session->getPage();
+    $page->fillField('label', 'Bears');
+    $page->fillField('id', 'bears');
+    $page->findButton(t('Save'))->click();
+
+    $session->getPage()->hasContent('Bears (bears)');
+
+    // Now edit that same workspace; We shouldn't be able to do so, since
+    // we don't have edit permissions.
+
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
+    $etm = \Drupal::service('entity_type.manager');
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $bears */
+    $entity_list = $etm->getStorage('workspace')->loadByProperties(['label' => 'Bears']);
+    $bears = current($entity_list);
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(403, $session->getStatusCode());
+
+    // @todo add Deletion checks once there's a UI for deletion.
+  }
+
+  /**
+   * Verifies that a user can create and edit only their own workspace.
+   */
+  public function testEditOwnWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+
+    // Now edit that same workspace; We should be able to do so.
+
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    $session = $this->getSession();
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $page = $session->getPage();
+    $page->fillField('label', 'Bears again');
+    $page->fillField('id', 'bears');
+    $page->findButton(t('Save'))->click();
+    $session->getPage()->hasContent('Bears again (bears)');
+
+    // Now login as a different user and ensure they don't have edit access,
+    // and vice versa.
+
+    $editor2 = $this->drupalCreateUser($permissions);
+
+    $this->drupalLogin($editor2);
+    $session = $this->getSession();
+
+    $this->createWorkspaceThroughUI('Packers', 'packers');
+
+    $packers = $this->getOneWorkspaceByLabel('Packers');
+
+    $this->drupalGet("/admin/structure/workspace/{$packers->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(403, $session->getStatusCode());
+  }
+
+  /**
+   * Verifies that a user can edit any workspace.
+   */
+  public function testEditAnyWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+
+    // Now edit that same workspace; We should be able to do so.
+
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    $session = $this->getSession();
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $page = $session->getPage();
+    $page->fillField('label', 'Bears again');
+    $page->fillField('id', 'bears');
+    $page->findButton(t('Save'))->click();
+    $session->getPage()->hasContent('Bears again (bears)');
+
+    // Now login as a different user and ensure they don't have edit access,
+    // and vice versa.
+
+    $admin = $this->drupalCreateUser(array_merge($permissions, ['edit any workspace']));
+
+    $this->drupalLogin($admin);
+    $session = $this->getSession();
+
+    $this->createWorkspaceThroughUI('Packers', 'packers');
+
+    $packers = $this->getOneWorkspaceByLabel('Packers');
+
+    $this->drupalGet("/admin/structure/workspace/{$packers->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    $this->drupalGet("/admin/structure/workspace/{$bears->id()}/edit");
+    $this->assertEquals(200, $session->getStatusCode());
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceSwitcherTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceSwitcherTest.php
new file mode 100644
index 0000000000..cc76c731b1
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceSwitcherTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests workspace switching functionality.
+ *
+ * @group workspace
+ */
+class WorkspaceSwitcherTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+  use BlockCreationTrait {
+    placeBlock as drupalPlaceBlock;
+  }
+
+  public static $modules = ['block', 'workspace'];
+
+  /**
+   * Test that the block displays and switches workspaces.
+   * Then test the admin page displays workspaces and allows switching.
+   */
+  public function testSwitchingWorkspaces() {
+    $permissions = [
+      'create workspace',
+      'edit own workspace',
+      'view own workspace',
+      'bypass entity access own workspace',
+    ];
+
+    $this->setupWorkspaceSwitcherBlock();
+
+    $mayer = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($mayer);
+
+    $vultures = $this->createWorkspaceThroughUI('Vultures', 'vultures');
+    $this->switchToWorkspace($vultures);
+
+    $gravity = $this->createWorkspaceThroughUI('Gravity', 'gravity');
+
+    $this->drupalGet('/admin/structure/workspace/' . $gravity->id() . '/activate');
+
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+    $page = $session->getPage();
+    $page->findButton(t('Activate'))->click();
+
+    $session->getPage()->findLink($gravity->label());
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceTest.php
new file mode 100644
index 0000000000..50d7a9ef95
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Test the workspace entity.
+ *
+ * @group workspace
+ */
+class WorkspaceTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+
+  public static $modules = ['workspace'];
+
+  public function testSpecialCharacters() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($editor1);
+
+    // Test a valid workspace name
+    $this->createWorkspaceThroughUI('Workspace 1', 'a0_$()+-/');
+
+    // Test and invaid workspace name
+    $this->drupalGet('/admin/structure/workspace/add');
+    $session = $this->getSession();
+    $this->assertEquals(200, $session->getStatusCode());
+    $page = $session->getPage();
+    $page->fillField('label', 'workspace2');
+    $page->fillField('id', 'A!"£%^&*{}#~@?');
+    $page->findButton(t('Save'))->click();
+    $session->getPage()->hasContent("This value is not valid");
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceTestUtilities.php b/core/modules/workspace/tests/src/Functional/WorkspaceTestUtilities.php
new file mode 100644
index 0000000000..1b0b2e96d2
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceTestUtilities.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\node\Entity\NodeType;
+use Drupal\workspace\Entity\WorkspaceInterface;
+
+/**
+ * Utility methods for use in BrowserTestBase tests.
+ *
+ * This trait will not work if not used in a child of BrowserTestBase.
+ */
+trait WorkspaceTestUtilities {
+
+  /**
+   * Loads a single workspace by its label.
+   *
+   * The UI approach to creating a workspace doesn't make it easy to know what
+   * the ID is, so this lets us make paths for a workspace after it's created.
+   *
+   * @param $label
+   *   The label of the workspace to load.
+   * @return \Drupal\workspace\Entity\WorkspaceInterface
+   */
+  protected function getOneWorkspaceByLabel($label) {
+    return $this->getOneEntityByLabel('workspace', $label);
+  }
+
+  /**
+   * Loads a single entity by its label.
+   *
+   * The UI approach to creating an entity doesn't make it easy to know what
+   * the ID is, so this lets us make paths for an entity after it's created.
+   *
+   * @param string $type
+   *   The type of entity to load.
+   * @param $label
+   *   The label of the entity to load.
+   * @return \Drupal\workspace\Entity\WorkspaceInterface
+   */
+  protected function getOneEntityByLabel($type, $label) {
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
+    $etm = \Drupal::service('entity_type.manager');
+
+    $property = $etm->getDefinition($type)->getKey('label');
+
+    $entity_list = $etm->getStorage($type)->loadByProperties([$property => $label]);
+
+    $entity = current($entity_list);
+
+    if (!$entity) {
+      $this->fail("No {$type} entity named {$label} found.");
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Creates a new Workspace through the UI.
+   *
+   * @param string $label
+   *   The label of the workspace to create.
+   * @param string $id
+   *   The ID of the workspace to create.
+   *
+   * @return \Drupal\workspace\Entity\WorkspaceInterface
+   *   The workspace that was just created.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function createWorkspaceThroughUI($label, $id) {
+    $this->drupalGet('/admin/structure/workspace/add');
+
+    $session = $this->getSession();
+    $this->assertSession()->statusCodeEquals(200);
+
+    /** @var \Behat\Mink\Element\DocumentElement $page */
+    $page = $session->getPage();
+    $page->fillField('label', $label);
+    $page->fillField('id', $id);
+    if ($id == 'dev') {
+      $page->selectFieldOption('upstream', 'workspace:stage');
+    }
+    $page->findButton(t('Save'))->click();
+
+    $session->getPage()->hasContent("$label ($id)");
+
+    return $this->getOneWorkspaceByLabel($label);
+  }
+
+  /**
+   * Adds the workspace switcher block to the site.
+   *
+   * This is necessary for switchToWorkspace() to function correctly.
+   */
+  protected function setupWorkspaceSwitcherBlock() {
+    // Add the block to the sidebar.
+    $this->drupalPlaceBlock('workspace_switcher_block', [
+      'id' => 'workspaceswitcher',
+      'region' => 'sidebar_first',
+      'label' => 'Workspace switcher',
+    ]);
+
+    // Confirm the block shows on the front page.
+    $this->drupalGet('<front>');
+    $page = $this->getSession()->getPage();
+
+    $this->assertTrue($page->hasContent('Workspace switcher'));
+  }
+
+  /**
+   * Sets a given workspace as "active" for subsequent requests.
+   *
+   * This assumes that the switcher block has already been setup by calling
+   * setupWorkspaceSwitcherBlock().
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace to set active.
+   */
+  protected function switchToWorkspace(WorkspaceInterface $workspace) {
+    // Switch the test runner's context to the specified workspace.
+    \Drupal::service('workspace.manager')->setActiveWorkspace($workspace);
+
+    // Switch the system under test to the specified workspace.
+    $this->getSession()->getPage()->findButton($workspace->label())->click();
+
+    // If we don't do both of those, test runner utility methods will not be
+    // run in the same workspace as the system under test, and you'll be left
+    // wondering why your test runner cannot find content you just created.
+  }
+
+  /**
+   * Creates a new node type.
+   *
+   * @param string $label
+   *   The human-readable label of the type to create.
+   * @param string $machine_name
+   *   The machine name of the type to create.
+   */
+  protected function createNodeType($label, $machine_name) {
+    $node_type = NodeType::create([
+      'type' => $machine_name,
+      'label' => $label,
+    ]);
+    $node_type->save();
+  }
+
+
+  /**
+   * Creates a node by "clicking" buttons.
+   *
+   * @param string $label
+   * @param string $bundle
+   * @param bool $publish
+   *
+   * @return \Drupal\workspace\Entity\WorkspaceInterface
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function createNodeThroughUI($label, $bundle, $publish = TRUE) {
+    $this->drupalGet('/node/add/' . $bundle);
+
+    /** @var \Behat\Mink\Session $session */
+    $session = $this->getSession();
+    $this->assertSession()->statusCodeEquals(200);
+
+    /** @var \Behat\Mink\Element\DocumentElement $page */
+    $page = $session->getPage();
+    $page->fillField('Title', $label);
+    if ($publish) {
+      $page->findButton(t('Save'))->click();
+    }
+    else {
+      $page->uncheckField('Published');
+      $page->findButton(t('Save'))->click();
+    }
+
+    $session->getPage()->hasContent("{$label} has been created");
+
+    return $this->getOneEntityByLabel('node', $label);
+  }
+
+  /**
+   * Returns a pointer to the specified workspace.
+   *
+   * @todo Replace this with a common method in the module somewhere.
+   *
+   * @param \Drupal\workspace\Entity\WorkspaceInterface $workspace
+   *   The workspace for which we want a pointer.
+   * @return \Drupal\workspace\WorkspacePointerInterface
+   *   The pointer to the provided workspace.
+   */
+  protected function getPointerToWorkspace(WorkspaceInterface $workspace) {
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
+    $etm = \Drupal::service('entity_type.manager');
+
+    $pointers = $etm->getStorage('workspace_pointer')
+      ->loadByProperties(['workspace_pointer' => $workspace->id()]);
+    $pointer = reset($pointers);
+    return $pointer;
+  }
+
+  /**
+   * Determine if the content list has an entity's label.
+   *
+   * This assertion can be used to validate a particular entity exists in the
+   * current workspace.
+   */
+  protected function isLabelInContentOverview($label) {
+    $this->drupalGet('/admin/content');
+    $session = $this->getSession();
+    $this->assertSession()->statusCodeEquals(200);
+    $page = $session->getPage();
+    return $page->hasContent($label);
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php
new file mode 100644
index 0000000000..b3aa981fa0
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional;
+
+use Drupal\simpletest\BrowserTestBase;
+
+
+/**
+ * Tests permission controls on workspaces.
+ *
+ * @group workspace
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class WorkspaceViewTest extends BrowserTestBase {
+  use WorkspaceTestUtilities;
+
+  public static $modules = ['workspace', 'workspace'];
+
+  /**
+   * Verifies that a user can view their own workspace.
+   */
+  public function testViewOwnWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+      'view own workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    // Now login as a different user and create a workspace.
+
+    $editor2 = $this->drupalCreateUser($permissions);
+
+    $this->drupalLogin($editor2);
+    $session = $this->getSession();
+
+    $this->createWorkspaceThroughUI('Packers', 'packers');
+
+    $packers = $this->getOneWorkspaceByLabel('Packers');
+
+    // Load the activate form for the Bears workspace. It should fail because
+    // the workspace belongs to someone else.
+    $this->drupalGet("admin/structure/workspace/{$bears->id()}/activate");
+    $this->assertEquals(403, $session->getStatusCode());
+
+    // But editor 2 should be able to activate the Packers workspace.
+    $this->drupalGet("admin/structure/workspace/{$packers->id()}/activate");
+    $this->assertEquals(200, $session->getStatusCode());
+  }
+
+  /**
+   * Verifies that a user can view any workspace.
+   */
+  public function testViewAnyWorkspace() {
+    $permissions = [
+      'access administration pages',
+      'administer site configuration',
+      'create workspace',
+      'edit own workspace',
+      'view any workspace',
+    ];
+
+    $editor1 = $this->drupalCreateUser($permissions);
+
+    // Login as a limited-access user and create a workspace.
+    $this->drupalLogin($editor1);
+
+    $this->createWorkspaceThroughUI('Bears', 'bears');
+
+    $bears = $this->getOneWorkspaceByLabel('Bears');
+
+    // Now login as a different user and create a workspace.
+
+    $editor2 = $this->drupalCreateUser($permissions);
+
+    $this->drupalLogin($editor2);
+    $session = $this->getSession();
+
+    $this->createWorkspaceThroughUI('Packers', 'packers');
+
+    $packers = $this->getOneWorkspaceByLabel('Packers');
+
+    // Load the activate form for the Bears workspace. This user should be
+    // able to see both workspaces because of the "view any" permission.
+    $this->drupalGet("admin/structure/workspace/{$bears->id()}/activate");
+    $this->assertEquals(200, $session->getStatusCode());
+
+    // But editor 2 should be able to activate the Packers workspace.
+    $this->drupalGet("admin/structure/workspace/{$packers->id()}/activate");
+    $this->assertEquals(200, $session->getStatusCode());
+  }
+
+}
diff --git a/core/modules/workspace/tests/src/Kernel/DatabaseStorageSortedSetTest.php b/core/modules/workspace/tests/src/Kernel/DatabaseStorageSortedSetTest.php
new file mode 100644
index 0000000000..871491c42f
--- /dev/null
+++ b/core/modules/workspace/tests/src/Kernel/DatabaseStorageSortedSetTest.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Drupal\Tests\workspace\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the sorted set key-value database storage.
+ *
+ * @group workspace
+ */
+class DatabaseStorageSortedSetTest extends KernelTestBase {
+
+
+  static public $modules = ['system', 'user', 'serialization', 'workspace'];
+
+  /**
+   * @var string
+   */
+  protected $collection;
+
+  /**
+   * @var \Drupal\Component\Serialization\SerializationInterface
+   */
+  protected $serializer;
+
+  /**
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreSortedSetInterface
+   */
+  protected $store;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->collection = $this->randomMachineName();
+    $this->serializer = \Drupal::service('serialization.phpserialize');
+    $this->connection = \Drupal::service('database');
+    $this->store = \Drupal::service('workspace.keyvalue.sorted_set')->get($this->collection);
+  }
+
+  /**
+   * Helper method to assert key value pairs.
+   *
+   * @param $expected_pairs array
+   *   Array of expected key value pairs.
+   */
+  public function assertPairs(array $expected_pairs) {
+    $result = $this->connection->select('key_value_sorted', 't')
+      ->fields('t', ['name', 'value'])
+      ->condition('collection', $this->collection)
+      ->condition('name', array_keys($expected_pairs), 'IN')
+      ->execute()
+      ->fetchAllAssoc('name');
+
+    $expected_count = count($expected_pairs);
+    $this->assertCount($expected_count, $result, "Query affected $expected_count records.");
+    foreach ($expected_pairs as $key => $value) {
+      $this->assertSame($value, $this->serializer->decode($result[$key]->value), "Key $key have value $value");
+    }
+  }
+
+  /**
+   * Helper method to assert the number of records.
+   *
+   * @param $expected int
+   *   Expected number of records.
+   * @param $message string
+   *   The message to display.
+   */
+  public function assertRecords($expected, $message = NULL) {
+    $count = $this->store->getCount();
+    $this->assertEquals($expected, $count, $message ? $message : "There are $expected records.");
+  }
+
+  /**
+   * Helper method to generate a key based on microtime().
+   *
+   * @return int
+   *   A key based on microtime().
+   */
+  public function newKey() {
+    return (int) (microtime(TRUE) * 1000000);
+  }
+
+  /**
+   * Tests getting and setting of sorted key value sets.
+   */
+  public function testCalls() {
+    $key0 = $this->newKey();
+    $value0 = $this->randomMachineName();
+    $this->store->add($key0, $value0);
+    $this->assertPairs([$key0 => $value0]);
+
+    $key1 = $this->newKey();
+    $value1 = $this->randomMachineName();
+    $this->store->add($key1, $value1);
+    $this->assertPairs([$key1 => $value1]);
+
+    // Ensure it works to add sets with the same key.
+    $key2 = $this->newKey();
+    $value2 = $this->randomMachineName();
+    $value3 = $this->randomMachineName();
+    $value4 = $this->randomMachineName();
+    $this->store->addMultiple([
+      [$key2 => $value2],
+      [$key2 => $value3],
+      [$key2 => $value4],
+    ]);
+
+    $this->assertRecords(5, 'Correct number of records in the collection.');
+
+    $value = $this->store->getRange($key1, $key2);
+    $this->assertSame([$value1, $value2, $value3, $value4], $value);
+
+    $value = $this->store->getRange($key1);
+    $this->assertSame([$value1, $value2, $value3, $value4], $value);
+
+    $new1 = $this->newKey();
+    $this->store->add($new1, $value1);
+
+    $value = $this->store->getRange($new1, $new1);
+    $this->assertSame([$value1], $value, 'Value was successfully updated.');
+    $this->assertRecords(5, 'Correct number of records in the collection after value update.');
+
+    $value = $this->store->getRange($key1, $key1);
+    $this->assertSame([], $value, 'Non-existing range returned empty array.');
+
+    $max_key = $this->store->getMaxKey();
+    $this->assertEquals($new1, $max_key, 'The getMaxKey method returned correct key.');
+
+    $min_key = $this->store->getMinKey();
+    $this->assertEquals($key0, $min_key, 'The getMinKey method returned correct key.');
+  }
+
+}
diff --git a/core/modules/workspace/workspace.info.yml b/core/modules/workspace/workspace.info.yml
new file mode 100644
index 0000000000..ec543d1d15
--- /dev/null
+++ b/core/modules/workspace/workspace.info.yml
@@ -0,0 +1,9 @@
+name: Workspace
+type: module
+description: 'Provides the ability to have multiple workspaces on a single site to facilitate things like full-site preview and content staging.'
+version: VERSION
+core: 8.x
+package: Core (Experimental)
+dependencies:
+ - user
+ - serialization
diff --git a/core/modules/workspace/workspace.install b/core/modules/workspace/workspace.install
new file mode 100644
index 0000000000..6ed94d839d
--- /dev/null
+++ b/core/modules/workspace/workspace.install
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains install, update and uninstall functions for the workspace module.
+ */
+
+use Drupal\workspace\Entity\Workspace;
+
+/**
+ * Implements hook_install().
+ */
+function workspace_install() {
+  /** @var \Drupal\workspace\Entity\WorkspaceInterface $live */
+  $live = Workspace::create(['id' => 'live', 'label' => 'Live']);
+  $live->save();
+
+  /** @var \Drupal\workspace\Entity\WorkspaceInterface $stage */
+  $stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
+  $stage->set('upstream', 'workspace:' . $live->id());
+  $stage->save();
+}
diff --git a/core/modules/workspace/workspace.libraries.yml b/core/modules/workspace/workspace.libraries.yml
new file mode 100644
index 0000000000..f38ed90503
--- /dev/null
+++ b/core/modules/workspace/workspace.libraries.yml
@@ -0,0 +1,28 @@
+drupal.workspace.toolbar:
+  version: VERSION
+  css:
+    theme:
+      css/workspace.toolbar.css: {}
+
+drupal.workspace.admin:
+  version: VERSION
+  css:
+    theme:
+      css/workspace.admin.css: {}
+
+drupal.workspace.switcher:
+  version: VERSION
+  css:
+    theme:
+      css/workspace.switcher.css: {}
+
+drupal.workspace.toolbox:
+  version: VERSION
+  css:
+    component:
+      css/workspace.toolbox.css: {}
+  js:
+    js/workspace.toolbox.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
diff --git a/core/modules/workspace/workspace.links.action.yml b/core/modules/workspace/workspace.links.action.yml
new file mode 100644
index 0000000000..9f22598f30
--- /dev/null
+++ b/core/modules/workspace/workspace.links.action.yml
@@ -0,0 +1,5 @@
+entity.workspace.add_form:
+  route_name: entity.workspace.add_form
+  title: 'Add workspace'
+  appears_on:
+    - entity.workspace.collection
diff --git a/core/modules/workspace/workspace.links.menu.yml b/core/modules/workspace/workspace.links.menu.yml
new file mode 100644
index 0000000000..fc925cfa2d
--- /dev/null
+++ b/core/modules/workspace/workspace.links.menu.yml
@@ -0,0 +1,5 @@
+entity.workspace.collection:
+  title: 'Workspaces'
+  parent: system.admin_structure
+  description: 'Create and manage workspaces.'
+  route_name: entity.workspace.collection
diff --git a/core/modules/workspace/workspace.module b/core/modules/workspace/workspace.module
new file mode 100644
index 0000000000..60c50a1b9b
--- /dev/null
+++ b/core/modules/workspace/workspace.module
@@ -0,0 +1,358 @@
+<?php
+
+/**
+ * @file
+ * Provides Workspaces for content.
+ */
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Drupal\views\Plugin\views\query\QueryPluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\workspace\EntityAccess;
+use Drupal\workspace\Form\DeploymentForm;
+use Drupal\workspace\Form\WorkspaceSwitcherForm;
+use Drupal\workspace\Plugin\Field\WorkspaceFieldItemList;
+
+/**
+ * Implements hook_help().
+ */
+function workspace_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the workspace module.
+    case 'help.page.workspace':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Workspace module allows workspaces to be defined and switched between. Content is then assigned to the active workspace when created. For more information, see the <a href=":workspace">online documentation for the Workspace module</a>.', [':workspace' => 'https://www.drupal.org/node/2824024']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function workspace_entity_base_field_info(EntityTypeInterface $entity_type) {
+  if (\Drupal::service('workspace.manager')->entityTypeCanBelongToWorkspaces($entity_type)) {
+    return ['workspace' => BaseFieldDefinition::create('workspace_reference')
+      ->setLabel(t('Workspace'))
+      ->setDescription(t('The Workspace of this piece of content.'))
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE)];
+  }
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function workspace_query_entity_query_alter(AlterableInterface $query) {
+  /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */
+  $workspace_manager = \Drupal::service('workspace.manager');
+  $active_workspace = $workspace_manager->getActiveWorkspace();
+  $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default');
+  if ($active_workspace == $default_workspace_id) {
+    return;
+  }
+
+  $entity_type = \Drupal::entityTypeManager()->getDefinition($query->getMetaData('entity_type'));
+  if (!empty($entity_type) && $workspace_manager->entityTypeCanBelongToWorkspaces($entity_type)) {
+    $entity_type_id = $entity_type->id();
+    $entity_type_id_key = $entity_type->getKey('id');
+    $query->leftJoin($entity_type->getRevisionDataTable(), $entity_type->getRevisionDataTable(), 'base_table.' . $entity_type_id_key . ' = ' . $entity_type->getRevisionDataTable() . '.' . $entity_type_id_key);
+    $query->condition($entity_type->getRevisionDataTable() . '.' . $entity_type_id_key, $entity_type_id);
+    $query->condition($entity_type->getRevisionDataTable() . '.workspace__target_id', [$active_workspace, $default_workspace_id], 'IN');
+  }
+}
+
+/**
+ * Implements hook_views_query_alter().
+ */
+function workspace_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
+  /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */
+  $workspace_manager = \Drupal::service('workspace.manager');
+  $active_workspace = $workspace_manager->getActiveWorkspace();
+  $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default');
+  if ($active_workspace == $default_workspace_id) {
+    return;
+  }
+
+  $entity_type = $view->getBaseEntityType();
+  if (empty($entity_type) || !$workspace_manager->entityTypeCanBelongToWorkspaces($entity_type)) {
+    return;
+  }
+  $configuration = [
+    'table' => $entity_type->getRevisionDataTable(),
+    'field' => $entity_type->getKey('id'),
+    'left_table' => $entity_type->getDataTable(),
+    'left_field' => $entity_type->getKey('id'),
+    'operator' => '=',
+  ];
+  /** @var \Drupal\views\Plugin\views\join\JoinPluginBase $join */
+  $join = \Drupal\views\Views::pluginManager('join')
+    ->createInstance('standard', $configuration);
+  /** @var \Drupal\views\Plugin\views\query\Sql $query */
+  $query->addRelationship($entity_type->getRevisionDataTable(), $join, $entity_type->getRevisionDataTable());
+  $query->setWhereGroup('OR', 'workspace');
+  $query->addWhere('workspace', $entity_type->getRevisionDataTable() . '.workspace__target_id', [$active_workspace, $default_workspace_id], 'IN');
+  $query->addWhere('workspace', $entity_type->getRevisionDataTable() . '.workspace__target_id', NULL, 'IS');
+  foreach ($query->where as $where_id => $where) {
+    foreach ($where['conditions'] as $condition_id => $condition) {
+      if ($condition['field'] == $entity_type->getDataTable() . '.' . $entity_type->getKey('published')) {
+        $value = $query->where[$where_id]['conditions'][$condition_id]['value'];
+        $query->setWhereGroup('OR', 'published');
+        $query->addWhere('published', $entity_type->getRevisionDataTable() . '.workspace__status', $value);
+        $query->addWhere('published', $entity_type->getDataTable() . '.' . $entity_type->getKey('published'), $value);
+        unset($query->where[$where_id]['conditions'][$condition_id]);
+      }
+    }
+  }
+
+  foreach ($view->displayHandlers->getInstanceIds() as $display_id) {
+    $view->displayHandlers->get($display_id)->setOption('group_by', TRUE);
+  }
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function workspace_entity_load(array &$entities, $entity_type_id) {
+  /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */
+  $workspace_manager = \Drupal::service('workspace.manager');
+  $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
+  if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity_type)) {
+    return;
+  }
+
+  $active_workspace = $workspace_manager->getActiveWorkspace();
+  $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default');
+  if ($active_workspace == $default_workspace_id) {
+    return;
+  }
+
+  $keys = array_keys($entities);
+  $results = \Drupal::entityTypeManager()
+    ->getStorage($entity_type_id)
+    ->getQuery()
+    ->allRevisions()
+    ->condition($entity_type->getKey('id'), $keys, 'IN')
+    ->condition('workspace.target_id', [$active_workspace, $default_workspace_id], 'IN')
+    ->sort($entity_type->getKey('revision'), 'DESC')
+    ->range(0, 1)
+    ->execute();
+  foreach ($results as $revision_id => $entity_id) {
+    $entity = $entities[$entity_id];
+    if ($revision_id != $entity->getRevisionId()) {
+      $new_entity = \Drupal::entityTypeManager()
+        ->getStorage($entity_type_id)
+        ->loadRevision($revision_id);
+      $entities[$entity_id] = $new_entity;
+    }
+    $entities[$entity_id]->workspace->status ? $entities[$entity_id]->setPublished() : $entities[$entity_id]->setUnpublished();
+  }
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function workspace_element_info_alter(array &$types) {
+  foreach ($types as &$type) {
+    if (!isset($type['#pre_render'])) {
+      $type['#pre_render'] = [];
+    }
+    $type['#pre_render'][] = 'workspace_element_pre_render';
+  }
+}
+
+/**
+ * Element pre-render callback.
+ */
+function workspace_element_pre_render($element) {
+  if (isset($element['#cache'])) {
+    if (!isset($element['#cache']['contexts'])) {
+      $element['#cache']['contexts'] = [];
+    }
+    $element['#cache']['contexts'] = Cache::mergeContexts(
+      $element['#cache']['contexts'], ['workspace']
+    );
+  }
+  return $element;
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function workspace_entity_presave(EntityInterface $entity) {
+  /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */
+  $workspace_manager = \Drupal::service('workspace.manager');
+
+  // Only modify the entity if the active workspace isn't the default, and
+  // and the entity can belong to a workspace.
+  if (!empty($workspace_manager->getActiveWorkspace())
+    && $workspace_manager->entityCanBelongToWorkspaces($entity)) {
+
+    if (!$entity->isNew()) {
+      $original_workspace_id = $entity->original->workspace->target_id;
+      $workspace_id = $entity->workspace->target_id;
+      /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
+      if ($original_workspace_id == $workspace_id) {
+        // Force a new revision is the entity is not new and the workspace
+        // is the same as the previous revision.
+        $entity->setNewRevision(TRUE);
+      }
+    }
+
+    // The publishing status can be stored in a property for safe keeping
+    $entity->initial_published = $entity->isPublished();
+
+    $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default');
+    if ($default_workspace_id != $workspace_manager->getActiveWorkspace()) {
+      // As this is the non-default workspace only new entity revisions should be
+      // made default.
+      if (isset($entity->original)
+          && ($entity->original->workspace->target_id == $default_workspace_id
+          || is_null($entity->original->workspace->target_id))) {
+        $entity->isDefaultRevision(FALSE);
+      }
+      else {
+        $entity->isDefaultRevision(TRUE);
+      }
+
+      // All entities in the non-default workspace get unpublished.
+      $entity->setUnpublished();
+    }
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function workspace_theme($existing, $type, $theme, $path) {
+  return [
+    'workspace_toolbox' => [
+      'variables' => [
+        'workspace_forms' => NULL,
+        'control_block' => NULL,
+        'deploy_form' => NULL,
+      ],
+    ],
+  ];
+}
+
+/**
+ * Implements hook_page_bottom().
+ */
+function workspace_page_bottom(array &$page_bottom) {
+  if (\Drupal::currentUser()->hasPermission('view own workspace') || \Drupal::currentUser()->hasPermission('view any workspace')) {
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $active_workspace */
+    $active_workspace = \Drupal::service('workspace.manager')
+      ->getActiveWorkspace(TRUE);
+    $control_block = t('Current workspace: <h3 class="active-workspace">@active_workspace</h3> <a href="@collection_url">Manage workspaces</a>', ['@active_workspace' => $active_workspace->label(),
+      '@collection_url' => Url::fromRoute('entity.workspace.collection')
+        ->toString()
+    ]);
+    $deploy_form = \Drupal::formBuilder()
+      ->getForm(DeploymentForm::class, $active_workspace);
+
+    $workspace_forms = [];
+    /** @var \Drupal\workspace\Entity\WorkspaceInterface $workspace */
+    foreach (\Drupal::entityTypeManager()->getStorage('workspace')->loadMultiple() as $workspace) {
+      if ($workspace->id() != $active_workspace->id() && $workspace->access('view', \Drupal::currentUser())) {
+        $workspace_forms['workspace_' . $workspace->id()] = \Drupal::formBuilder()
+          ->getForm(WorkspaceSwitcherForm::class, $workspace);
+      }
+    }
+
+    $page_bottom['workspace_toolbox'] = [
+      '#theme' => 'workspace_toolbox',
+      '#workspace_forms' => $workspace_forms,
+      '#control_block' => $control_block,
+      '#deploy_form' => $deploy_form,
+      '#attached' => [
+        'library' => ['workspace/drupal.workspace.toolbox'],
+      ],
+    ];
+  }
+}
+
+/**
+ * Implements hook_toolbar().
+ *
+ * @see \Drupal\workspace\Toolbar
+ */
+function workspace_toolbar() {
+  $items = [];
+
+  /** @var \Drupal\workspace\Entity\WorkspaceInterface $active_workspace */
+  $active_workspace = \Drupal::service('workspace.manager')->getActiveWorkspace(TRUE);
+
+  $items['workspace_switcher'] = [
+    // Include the toolbar_tab_wrapper to style the link like a toolbar tab.
+    // Exclude the theme wrapper if custom styling is desired.
+    '#type' => 'toolbar_item',
+    '#weight' => 125,
+    '#wrapper_attributes' => [
+      'class' => ['workspace-toolbar-tab'],
+    ],
+    '#attached' => [
+      'library' => ['workspace/drupal.workspace.toolbar'],
+    ],
+  ];
+
+  $items['workspace_switcher']['tab'] = [
+    '#type' => 'link',
+    '#title' => t('@active', ['@active' => $active_workspace->label()]),
+    '#url' => Url::fromRoute('entity.workspace.collection'),
+    '#attributes' => [
+      'title' => t('Switch workspaces'),
+      'class' => ['toolbar-icon', 'toolbar-icon-workspace'],
+    ],
+  ];
+
+  return $items;
+}
+
+/**
+ * Prerender callback; Adds the workspace switcher forms to the render array.
+ *
+ * @param array $element
+ *
+ * @return array
+ *   The modified $element.
+ */
+function workspace_switcher_toolbar_pre_render(array $element) {
+  /** @var \Drupal\workspace\Entity\WorkspaceInterface $workspace */
+  foreach (\Drupal::entityTypeManager()->getStorage('workspace')->loadMultiple() as $workspace) {
+    if ($workspace->access('view', \Drupal::currentUser())) {
+      $element['workspace_forms']['workspace_' . $workspace->id()] = \Drupal::formBuilder()->getForm(WorkspaceSwitcherForm::class, $workspace);
+    }
+  }
+
+  return $element;
+}
+
+/**
+ * Implements hook_entity_access().
+ *
+ * @see \Drupal\workspace\EntityAccess
+ */
+function workspace_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
+  return \Drupal::service('class_resolver')
+    ->getInstanceFromDefinition(EntityAccess::class)
+    ->entityAccess($entity, $operation, $account);
+}
+
+/**
+ * Implements hook_entity_create_access().
+ *
+ * @see \Drupal\workspace\EntityAccess
+ */
+function workspace_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
+  return \Drupal::service('class_resolver')
+    ->getInstanceFromDefinition(EntityAccess::class)
+    ->entityCreateAccess($account, $context, $entity_bundle);
+}
diff --git a/core/modules/workspace/workspace.permissions.yml b/core/modules/workspace/workspace.permissions.yml
new file mode 100644
index 0000000000..36a12e1654
--- /dev/null
+++ b/core/modules/workspace/workspace.permissions.yml
@@ -0,0 +1,34 @@
+administer workspaces:
+  title: Administer workspaces
+
+create workspace:
+  title: Create a new workspace
+
+view own workspace:
+  title: View own workspace
+
+view any workspace:
+  title: View any workspace
+
+edit own workspace:
+  title: Edit own workspace
+
+edit any workspace:
+  title: Edit any workspace
+
+delete own workspace:
+  title: Delete own workspace
+
+delete any workspace:
+  title: Delete any workspace
+
+bypass entity access own workspace:
+  title: Bypass content entity access in own workspace
+  description: Allow all Edit/Update/Delete permissions for all content entities in a workspace owned by the user.
+  restrict access: TRUE
+
+update any workspace from upstream:
+  title: Update any workspace from upstream
+
+permission_callbacks:
+  - Drupal\workspace\EntityAccess::workspacePermissions
diff --git a/core/modules/workspace/workspace.routing.yml b/core/modules/workspace/workspace.routing.yml
new file mode 100644
index 0000000000..c2b03cedc0
--- /dev/null
+++ b/core/modules/workspace/workspace.routing.yml
@@ -0,0 +1,27 @@
+entity.workspace.collection:
+  path: '/admin/structure/workspace'
+  defaults:
+    _title: 'Workspaces'
+    _entity_list: 'workspace'
+  requirements:
+    _permission: 'administer workspaces+edit any workspace'
+
+entity.workspace.activate_form:
+  path: '/admin/structure/workspace/{workspace}/activate'
+  defaults:
+    _title: 'Activate Workspace'
+    _form: '\Drupal\workspace\Form\WorkspaceActivateForm'
+  options:
+    _admin_route: TRUE
+  requirements:
+    _workspace_view: 'TRUE'
+
+entity.workspace.deployment_form:
+  path: '/admin/structure/workspace/{workspace}/deployment'
+  defaults:
+    _title: 'Workspace deployment'
+    _controller: '\Drupal\workspace\Controller\DeploymentController::workspaces'
+  options:
+    _admin_route: TRUE
+  requirements:
+    _permission: 'administer workspaces'
diff --git a/core/modules/workspace/workspace.services.yml b/core/modules/workspace/workspace.services.yml
new file mode 100644
index 0000000000..4865fa16a2
--- /dev/null
+++ b/core/modules/workspace/workspace.services.yml
@@ -0,0 +1,69 @@
+parameters:
+  workspace.default: live
+  workspace.factory.keyvalue.sorted_set:
+    default: keyvalue.sorted_set.database
+
+services:
+  workspace.paramconverter.entity_revision:
+    class: Drupal\workspace\ParamConverter\EntityRevisionConverter
+    arguments: ['@entity.manager']
+    tags:
+      - { name: paramconverter, priority: 30 }
+  access_check.workspace_view:
+    class: Drupal\workspace\Access\WorkspaceViewCheck
+    tags:
+      - { name: access_check, applies_to: _workspace_view }
+  workspace.manager:
+    class: Drupal\workspace\WorkspaceManager
+    arguments: ['@request_stack', '@entity_type.manager', '@current_user', '@logger.channel.workspace']
+    tags:
+      - { name: service_collector, tag: workspace_negotiator, call: addNegotiator }
+  workspace.index.sequence:
+    class: Drupal\workspace\Index\SequenceIndex
+    arguments: ['@workspace.keyvalue.sorted_set', '@workspace.manager']
+  workspace.changes_factory:
+    class: Drupal\workspace\Changes\ChangesFactory
+    arguments: ['@entity_type.manager', '@workspace.index.sequence']
+  workspace.keyvalue.sorted_set:
+    class: Drupal\workspace\KeyValueStore\KeyValueSortedSetFactory
+    arguments: ['@service_container', '%workspace.factory.keyvalue.sorted_set%']
+  workspace.keyvalue.sorted_set.database:
+    class: Drupal\workspace\KeyValueStore\KeyValueDatabaseSortedSetFactory
+    arguments: ['@serialization.phpserialize', '@database']
+  workspace.upstream_manager:
+    class: Drupal\workspace\UpstreamManager
+    parent: default_plugin_manager
+
+  workspace.replication_manager:
+    class: Drupal\workspace\Replication\ReplicationManager
+    tags:
+      - { name: service_collector, tag: workspace_replicator, call: addReplicator }
+  workspace.default_replicator:
+    class: Drupal\workspace\Replication\DefaultReplicator
+    arguments: ['@workspace.manager', '@workspace.changes_factory', '@entity_type.manager', '@workspace.index.sequence']
+    tags:
+      - { name: workspace_replicator, priority: 0 }
+
+  workspace.negotiator.default:
+    class: Drupal\workspace\Negotiator\DefaultWorkspaceNegotiator
+    arguments: ['%workspace.default%']
+    tags:
+      - { name: workspace_negotiator, priority: 0 }
+  workspace.negotiator.session:
+    class: Drupal\workspace\Negotiator\SessionWorkspaceNegotiator
+    arguments: ['@current_user', '@user.private_tempstore', '%workspace.default%']
+    tags:
+      - { name: workspace_negotiator, priority: 100 }
+  workspace.negotiator.param:
+    class: Drupal\workspace\Negotiator\ParamWorkspaceNegotiator
+    tags:
+      - { name: workspace_negotiator, priority: 200 }
+
+  cache_context.workspace:
+    class: Drupal\workspace\WorkspaceCacheContext
+    arguments: ['@workspace.manager']
+    tags:
+      - { name: cache.context }
+  logger.channel.workspace:
+    parent: logger.channel_base
+    arguments: ['cron']
