diff --git a/core/modules/moderation/moderation.info.yml b/core/modules/moderation/moderation.info.yml
new file mode 100644
index 0000000..b963683
--- /dev/null
+++ b/core/modules/moderation/moderation.info.yml
@@ -0,0 +1,8 @@
+name: Moderation
+type: module
+description: 'Moderate revisionable entities between states.'
+package: Core
+version: VERSION
+core: 8.x
+dependencies:
+  - options
diff --git a/core/modules/moderation/moderation.links.task.yml b/core/modules/moderation/moderation.links.task.yml
new file mode 100644
index 0000000..7957ee9
--- /dev/null
+++ b/core/modules/moderation/moderation.links.task.yml
@@ -0,0 +1,4 @@
+node.draft:
+  title: Draft
+  route_name: node.draft
+  base_route: entity.node.canonical
diff --git a/core/modules/moderation/moderation.module b/core/modules/moderation/moderation.module
new file mode 100644
index 0000000..a264243
--- /dev/null
+++ b/core/modules/moderation/moderation.module
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Moderation of revisionable entities.
+ */
+
+use \Drupal\node\NodeInterface;
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function moderation_entity_type_alter(array &$entity_types) {
+  if (isset($entity_types['node'])) {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $entity_types['node'];
+
+    // Swap in a different form handler to ensure the proper version is loaded.
+    $handlers = $entity_type->getHandlerClasses();
+    $handlers['form']['default'] = 'Drupal\moderation\Form\NodeForm';
+    $handlers['form']['edit'] = $handlers['form']['default'];
+    $entity_type->setHandlerClass('form', $handlers['form']);
+  }
+}
+
+/**
+ * Determine if the given node has a draft.
+ *
+ * Draft is simply defined here as an unpublished revision that is newer than
+ * the published node.
+ *
+ * @param \Drupal\core\NodeInterface $node
+ *   The node object.
+ *
+ * @return bool|int
+ *   The revision ID if a draft exists, otherwise FALSE.
+ */
+function moderation_node_has_draft(NodeInterface $node) {
+  $current_revision = $node->getRevisionId();
+  $node_storage = \Drupal::service('entity.manager')->getStorage('node');
+  $vids = $node_storage->revisionIds($node);
+
+  // Filter out vids less than or equal to current revision.
+  asort($vids);
+  $filtered = array_filter($vids, function ($var) use ($current_revision) {
+    return $var > $current_revision;
+  });
+  return array_pop($filtered) ?: FALSE;
+}
diff --git a/core/modules/moderation/moderation.routing.yml b/core/modules/moderation/moderation.routing.yml
new file mode 100644
index 0000000..b52bd23
--- /dev/null
+++ b/core/modules/moderation/moderation.routing.yml
@@ -0,0 +1,7 @@
+node.draft:
+  path: '/node/{node}/draft'
+  defaults:
+    _controller: 'Drupal\moderation\Controller\DraftController::show'
+    _title_callback: 'Drupal\moderation\Controller\DraftController::draftPageTitle'
+  requirements:
+    _access_node_draft: 'view'
diff --git a/core/modules/moderation/moderation.services.yml b/core/modules/moderation/moderation.services.yml
new file mode 100644
index 0000000..08193dc
--- /dev/null
+++ b/core/modules/moderation/moderation.services.yml
@@ -0,0 +1,6 @@
+services:
+  access_check.node.draft:
+    class: Drupal\moderation\Access\DraftAccess
+    arguments: ['@entity.manager']
+    tags:
+      - { name: access_check, applies_to: _access_node_draft }
diff --git a/core/modules/moderation/src/Access/DraftAccess.php b/core/modules/moderation/src/Access/DraftAccess.php
new file mode 100644
index 0000000..13bd137
--- /dev/null
+++ b/core/modules/moderation/src/Access/DraftAccess.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\moderation\Access\DraftAccess.
+ */
+
+namespace Drupal\moderation\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides an access checker for node revisions.
+ */
+class DraftAccess implements AccessInterface {
+
+  /**
+   * The node storage.
+   *
+   * @var \Drupal\node\NodeStorageInterface
+   */
+  protected $nodeStorage;
+
+  /**
+   * The node access control handler.
+   *
+   * @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
+   */
+  protected $nodeAccess;
+
+  /**
+   * Constructs a new DraftAccess.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface
+   *   The entity manager.
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->nodeStorage = $entity_manager->getStorage('node');
+    $this->nodeAccess = $entity_manager->getAccessControlHandler('node');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, AccountInterface $account, NodeInterface $node = NULL) {
+    // Check that the user has the ability to update the node, and that the node
+    // has a draft.
+    return AccessResult::allowedIf($node->access('update', $account) && moderation_node_has_draft($node));
+  }
+
+}
diff --git a/core/modules/moderation/src/Controller/DraftController.php b/core/modules/moderation/src/Controller/DraftController.php
new file mode 100644
index 0000000..63f7111
--- /dev/null
+++ b/core/modules/moderation/src/Controller/DraftController.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\moderation\Controller\DraftController.
+ */
+
+namespace Drupal\moderation\Controller;
+
+use Drupal\node\Controller\NodeController;
+use Drupal\node\NodeInterface;
+
+/**
+ * Page controller for viewing node drafts.
+ */
+class DraftController extends NodeController {
+
+  /**
+   * Display current revision denoted as a draft.
+   *
+   * @param \Drupal\node\NodeInterface
+   *   The current node.
+   */
+  public function show(NodeInterface $node) {
+    return $this->revisionShow(moderation_node_has_draft($node));
+  }
+
+  /**
+   * Display the title of the draft.
+   */
+  public function draftPageTitle(NodeInterface $node) {
+    return $this->revisionPageTitle(moderation_node_has_draft($node));
+  }
+
+}
diff --git a/core/modules/moderation/src/Form/NodeForm.php b/core/modules/moderation/src/Form/NodeForm.php
new file mode 100644
index 0000000..4545a25
--- /dev/null
+++ b/core/modules/moderation/src/Form/NodeForm.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\moderation\Form\NodeForm
+ */
+
+namespace Drupal\moderation\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\node\NodeForm as BaseNodeForm;
+
+/**
+ * Override the node form.
+ */
+class NodeForm extends BaseNodeForm {
+
+  /**
+   * Track if this is a draft.
+   *
+   * @var bool
+   */
+  protected $isDraft = FALSE;
+
+  /**
+   * Ensure proper node revision is used in the node form.
+   *
+   * {@inheritdoc}
+   */
+  protected function prepareEntity() {
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $this->getEntity();
+    if (!$node->isNew() && $node->type->entity->isNewRevision() && $revision_id = moderation_node_has_draft($node)) {
+      /** @var \Drupal\node\NodeStorage $storage */
+      $storage = \Drupal::service('entity.manager')->getStorage('node');
+      $this->entity = $storage->loadRevision($revision_id);
+      $this->isDraft = TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $element = parent::actions($form, $form_state);
+
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $this->getEntity();
+    if (!$node->isNew() && $node->type->entity->isNewRevision() && $node->isPublished()) {
+      // Add a 'save as draft' action.
+      $element['draft'] = $element['submit'];
+      $element['draft']['#access'] = TRUE;
+      $element['draft']['#dropbutton'] = 'save';
+      $element['draft']['#value'] = $this->t('Save as draft');
+      // Setting to draft must be called before ::save, while setting the
+      // redirect must be done after.
+      array_unshift($element['draft']['#submit'], '::draft');
+      $element['draft']['#submit'][] = '::setRedirect';
+
+      // Put the draft button first.
+      $element['draft']['#weight'] = -10;
+
+      // If the user doesn't have 'administer nodes' permission, and this is
+      // a published node in a type that defaults to being unpublished, then
+      // only allow new drafts.
+      if (!\Drupal::currentUser()->hasPermission('administer nodes') && $this->nodeTypeUnpublishedDefault()) {
+        $element['submit']['#access'] = FALSE;
+        unset($element['draft']['#dropbutton']);
+      }
+    }
+
+    // If this is an existing draft, change the publish button text.
+    if ($this->isDraft && isset($element['publish'])) {
+      $element['publish']['#value'] = t('Save and publish');
+    }
+
+    return $element;
+  }
+
+  /**
+   * Save node as a draft.
+   */
+  public function draft(array $form, FormStateInterface $form_state) {
+    $this->entity->isDefaultRevision(FALSE);
+  }
+
+  /**
+   * Set default revision if this was previously a draft, and is now being
+   * published.
+   *
+   * {@inheritdoc}
+   */
+  public function publish(array $form, FormStateInterface $form_state) {
+    $node = parent::publish($form, $form_state);
+    if ($this->isDraft) {
+      $node->isDefaultRevision(TRUE);
+    }
+  }
+
+  /**
+   * Set a redirect to the draft.
+   */
+  public function setRedirect(array $form, FormStateInterface $form_state) {
+    $form_state->setRedirect(
+      'node.draft',
+      array('node' => $this->getEntity()->id())
+    );
+  }
+
+  /**
+   * Helper function to determine unpublished default for a node type.
+   *
+   * @return bool
+   *   Returns TRUE if the current node type is set to unpublished by default.
+   */
+  protected function nodeTypeUnpublishedDefault() {
+    $type = $this->getEntity()->getType();
+    // @todo Make it possible to get default values without an entity.
+    //   https://www.drupal.org/node/2318187
+    $node = $this->entityManager->getStorage('node')->create(array('type' => $type));
+    return !$node->isPublished();
+  }
+
+}
+
+
diff --git a/core/modules/moderation/src/Tests/ModerationNodeTest.php b/core/modules/moderation/src/Tests/ModerationNodeTest.php
new file mode 100644
index 0000000..aee316d
--- /dev/null
+++ b/core/modules/moderation/src/Tests/ModerationNodeTest.php
@@ -0,0 +1,173 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\moderation\Tests\ModerationNodeTest.
+ */
+
+namespace Drupal\moderation\Tests;
+
+use Drupal\node\Entity\NodeType;
+use Drupal\node\Tests\AssertButtonsTrait;
+use Drupal\node\Tests\NodeTestBase;
+
+/**
+ * Tests Creation and editing of forward revisions of a node.
+ *
+ * @group moderation
+ */
+class ModerationNodeTest extends NodeTestBase {
+
+  use AssertButtonsTrait;
+
+  /**
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser, $normalUser;
+
+  /**
+   * @var \Drupal\node\NodeTypeInterface
+   */
+  protected $nodeType;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('node', 'moderation');
+
+  /**
+   * {@inheritdoc}
+   */
+  function setUp() {
+    parent::setUp();
+
+    // Use revisions by default.
+    $this->nodeType = NodeType::load('article');
+    $this->nodeType->setNewRevision(TRUE);
+    $this->nodeType->save();
+
+    // Set unpublished by default.
+    $fields = \Drupal::entityManager()->getFieldDefinitions('node', 'article');
+    $fields['status']->getConfig('article')
+      ->setDefaultValue(FALSE)
+      ->save();
+
+    // Set up users.
+    $this->adminUser = $this->drupalCreateUser(['bypass node access', 'administer nodes']);
+    $this->normalUser = $this->drupalCreateUser(['create article content', 'edit own article content']);
+  }
+
+  /**
+   * Checks node edit functionality.
+   */
+  function testBasicForwarRevisions() {
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('node/add/article');
+
+    // For new content, the 'Save as draft' button should not be present.
+    $this->assertButtons([t('Save as unpublished'), t('Save and publish')]);
+
+    // Create an unpublished node.
+    $unpublished_one_values = [
+      // While standard to use randomName() for these values, human-readable
+      // text is easier to debug in this context.
+      'title[0][value]' => 'Unpublished one title',
+      'body[0][value]' => 'Unpublished one body',
+    ];
+    $this->drupalPostForm('node/add/article', $unpublished_one_values, t('Save as unpublished'));
+    $node = $this->drupalGetNodeByTitle($unpublished_one_values['title[0][value]']);
+
+    // Verify that the admin user can see the node's page.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertResponse(200, t('Draft is accessible to the admin user.'));
+
+    // There should not be a draft tab since there is only a single unpublished
+    // revision.
+    $this->assertNoLink(t('Draft'));
+
+    // Make another revision and publish.
+    $this->drupalGet('node/' . $node->id() .'/edit');
+    $this->assertButtons([t('Save and keep unpublished'), t('Save and publish')]);
+    $published_one_values = [
+      'title[0][value]' => 'Published one title',
+      'body[0][value]' => 'Published one title',
+    ];
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $published_one_values, t('Save and publish'));
+
+    // Verify published values are on the node page.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertText($published_one_values['title[0][value]'], 'Published title found');
+    $this->assertText($published_one_values['body[0][value]'], t('Published body found'));
+
+    // There should still be no draft tab since the latest revision is
+    // published.
+    $this->assertNoLink(t('Draft'));
+
+    // Create a draft.
+    $this->drupalGet('node/' . $node->id() . '/edit');
+    $this->assertButtons([t('Save as draft'), t('Save and keep published'), t('Save and unpublish')]);
+    $draft_one_values = [
+      'title[0][value]' => 'Draft one title',
+      'body[0][value]' => 'Draft one body',
+    ];
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $draft_one_values, t('Save as draft'));
+
+    // The user should be redirected to the draft.
+    $this->assertUrl('node/' . $node->id() . '/draft', [], 'User is redirected to view the draft after saving.');
+
+    // Now there should be a draft tab.
+    $this->assertLink(t('Draft'));
+
+    // Verify that the admin user can see the node's page.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertText($published_one_values['title[0][value]'], 'The published title stays the same when a new draft is created.');
+    $this->assertText($published_one_values['body[0][value]'], 'The published body stays the same when a new draft is created.');
+
+    // The draft should be loaded in the edit form.
+    $this->drupalGet('node/' . $node->id() . '/edit');
+    $this->assertRaw($draft_one_values['title[0][value]'], 'The draft title is loaded on the edit form.');
+    $this->assertRaw($draft_one_values['body[0][value]'], 'The draft body is loaded on the edit form.');
+    $this->assertButtons([t('Save as draft'), t('Save and publish'), t('Save and unpublish')]);
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $draft_one_values, t('Save and publish'));
+
+    // Publish the draft.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertText($draft_one_values['title[0][value]'], 'Published title found');
+    $this->assertText($draft_one_values['body[0][value]'], t('Published body found'));
+
+    // For normal users (eg, users without the administer nodes permission), if
+    // a content type is set to be unpublished by default, then on edits, only
+    // allow new drafts to be created, rather than allowing the published node
+    // to be updated.
+    $this->drupalLogin($this->normalUser);
+    $this->drupalGet('node/add/article');
+    $this->assertButtons([t('Save')], FALSE);
+
+    // Create a node and publish.
+    $this->drupalPostForm('node/add/article', $published_one_values, t('Save'));
+    $node = $this->drupalGetNodeByTitle($published_one_values['title[0][value]']);
+    $node->setPublished(TRUE)->save();
+
+    // Edit the node.
+    $this->drupalGet('node/' . $node->id() . '/edit');
+    $this->assertButtons([t('Save as draft')], FALSE, t('Save as draft'));
+    $edit = [
+      'title[0][value]' => 'Draft one title',
+      'body[0][value]' => 'Draft one body',
+    ];
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save as draft'));
+    $this->assertLink(t('Draft'));
+
+    // User with view, but not update, permissions, should not see the draft
+    // tab.
+    $this->drupalLogout();
+    $this->drupalGet('node/' . $node->id());
+    $this->assertNoLink(t('Draft'), 'The draft tab does not appear for users without update access.');
+
+    // And should not be able to access it directly either.
+    $this->drupalGet('node/' . $node->id() . '/draft');
+    $this->assertResponse(403, 'Access is denied for the draft page for users without update access.');
+  }
+
+}
+
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index 17066ab..b64d77b 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -352,6 +352,9 @@ public function preview(array $form, FormStateInterface $form_state) {
    *   An associative array containing the structure of the form.
    * @param $form_state
    *   The current state of the form.
+   *
+   * @return \Drupal\node\NodeInterface
+   *   The node object.
    */
   public function publish(array $form, FormStateInterface $form_state) {
     $node = $this->entity;
@@ -366,6 +369,9 @@ public function publish(array $form, FormStateInterface $form_state) {
    *   An associative array containing the structure of the form.
    * @param $form_state
    *   The current state of the form.
+   *
+   * @return \Drupal\node\NodeInterface
+   *   The node object.
    */
   public function unpublish(array $form, FormStateInterface $form_state) {
     $node = $this->entity;
diff --git a/core/modules/node/src/Tests/AssertButtonsTrait.php b/core/modules/node/src/Tests/AssertButtonsTrait.php
new file mode 100644
index 0000000..3d9b357
--- /dev/null
+++ b/core/modules/node/src/Tests/AssertButtonsTrait.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\AssertButtonsTrait.
+ */
+
+namespace Drupal\node\Tests;
+
+/**
+ * Asserts that buttons are present on a page.
+ */
+trait AssertButtonsTrait {
+
+  /**
+   * Assert method to verify the buttons in the dropdown element.
+   *
+   * @param array $buttons
+   *   A collection of buttons to assert for on the page.
+   * @param bool $dropbutton
+   *   Whether to check if the buttons are in a dropbutton widget or not.
+   * @param string $save_button_value
+   *   The value that should be present for the primary submit button.
+   */
+  public function assertButtons($buttons, $dropbutton = TRUE, $save_button_value = 'Save') {
+
+    // Try to find a Save button.
+    $save_button = $this->xpath('//input[@type="submit"][@value="' . $save_button_value . '"]');
+
+    // Verify that the number of buttons passed as parameters is
+    // available in the dropbutton widget.
+    if ($dropbutton) {
+      $i = 0;
+      $count = count($buttons);
+
+      // Assert there is no save button.
+      $this->assertTrue(empty($save_button));
+
+      // Dropbutton elements.
+      $elements = $this->xpath('//div[@class="dropbutton-wrapper"]//input[@type="submit"]');
+      $this->assertEqual($count, count($elements));
+      foreach ($elements as $element) {
+        $value = isset($element['value']) ? (string) $element['value'] : '';
+        $this->assertEqual($buttons[$i], $value);
+        $i++;
+      }
+    }
+    else {
+      // Assert there is a save button.
+      $this->assertTrue(!empty($save_button));
+      $this->assertNoRaw('dropbutton-wrapper');
+    }
+  }
+
+}
diff --git a/core/modules/node/src/Tests/NodeFormButtonsTest.php b/core/modules/node/src/Tests/NodeFormButtonsTest.php
index b5c2df5..8fe115a 100644
--- a/core/modules/node/src/Tests/NodeFormButtonsTest.php
+++ b/core/modules/node/src/Tests/NodeFormButtonsTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\node\Tests;
 
+use Drupal\node\Tests\AssertButtonsTrait;
+
 /**
  * Tests all the different buttons on the node form.
  *
@@ -14,6 +16,8 @@
  */
 class NodeFormButtonsTest extends NodeTestBase {
 
+  use AssertButtonsTrait;
+
   /**
    * A normal logged in user.
    *
@@ -135,41 +139,4 @@ function testNodeFormButtons() {
     $this->assertFalse($node_3->isPublished(), 'Node is unpublished');
   }
 
-  /**
-   * Assert method to verify the buttons in the dropdown element.
-   *
-   * @param array $buttons
-   *   A collection of buttons to assert for on the page.
-   * @param bool $dropbutton
-   *   Whether to check if the buttons are in a dropbutton widget or not.
-   */
-  public function assertButtons($buttons, $dropbutton = TRUE) {
-
-    // Try to find a Save button.
-    $save_button = $this->xpath('//input[@type="submit"][@value="Save"]');
-
-    // Verify that the number of buttons passed as parameters is
-    // available in the dropbutton widget.
-    if ($dropbutton) {
-      $i = 0;
-      $count = count($buttons);
-
-      // Assert there is no save button.
-      $this->assertTrue(empty($save_button));
-
-      // Dropbutton elements.
-      $elements = $this->xpath('//div[@class="dropbutton-wrapper"]//input[@type="submit"]');
-      $this->assertEqual($count, count($elements));
-      foreach ($elements as $element) {
-        $value = isset($element['value']) ? (string) $element['value'] : '';
-        $this->assertEqual($buttons[$i], $value);
-        $i++;
-      }
-    }
-    else {
-      // Assert there is a save button.
-      $this->assertTrue(!empty($save_button));
-      $this->assertNoRaw('dropbutton-wrapper');
-    }
-  }
 }
