diff --git a/.travis.yml b/.travis.yml
index eeaee76..06520bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,12 +23,21 @@ addons:
 
 env:
   matrix:
-    - SCRIPT='modules/relaxed/tests/bin/drupal.sh' MAKE_FILE=drupal-8.4.x.make.yml
-    - SCRIPT='modules/relaxed/tests/bin/pouchdb.sh' MAKE_FILE=drupal-8.4.x.make.yml
-    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.4.x.make.yml COUCHDB_VERSION=2.0-dev
-    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.4.x.make.yml COUCHDB_VERSION=1.6.1
+    - SCRIPT='modules/relaxed/tests/bin/drupal.sh' MAKE_FILE=drupal-8.2.x.make.yml
+    - SCRIPT='modules/relaxed/tests/bin/pouchdb.sh' MAKE_FILE=drupal-8.2.x.make.yml
+    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.2.x.make.yml COUCHDB_VERSION=2.0-dev
+    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.2.x.make.yml COUCHDB_VERSION=1.6.1
+    - SCRIPT='modules/relaxed/tests/bin/drupal.sh' MAKE_FILE=drupal-8.3.x.make.yml
+    - SCRIPT='modules/relaxed/tests/bin/pouchdb.sh' MAKE_FILE=drupal-8.3.x.make.yml
+    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.3.x.make.yml COUCHDB_VERSION=2.0-dev
+    - SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.3.x.make.yml COUCHDB_VERSION=1.6.1
 
 matrix:
+  allow_failures:
+    - env: SCRIPT='modules/relaxed/tests/bin/drupal.sh' MAKE_FILE=drupal-8.2.x.make.yml
+    - env: SCRIPT='modules/relaxed/tests/bin/pouchdb.sh' MAKE_FILE=drupal-8.2.x.make.yml
+    - env: SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.2.x.make.yml COUCHDB_VERSION=2.0-dev
+    - env: SCRIPT='modules/relaxed/tests/bin/replication.sh' MAKE_FILE=drupal-8.2.x.make.yml COUCHDB_VERSION=1.6.1
   fast_finish: true
 
 notifications:
diff --git a/.travis/drupal-8.4.x.make.yml b/.travis/drupal-8.2.x.make.yml
similarity index 94%
rename from .travis/drupal-8.4.x.make.yml
rename to .travis/drupal-8.2.x.make.yml
index 6d9c70d..0e553a6 100644
--- a/.travis/drupal-8.4.x.make.yml
+++ b/.travis/drupal-8.2.x.make.yml
@@ -3,7 +3,7 @@ api: 2
 core: 8.x
 
 projects:
-  drupal: 8.4.x
+  drupal: 8.2.x
   key_value: 1.x
   conflict:
     type: module
@@ -21,7 +21,7 @@ projects:
     type: module
     download:
       type: git
-      branch: 8.x-2.x
+      branch: 8.x-1.x
       url: "https://github.com/relaxedws/drupal-replication.git"
   workspace:
     type: module
diff --git a/.travis/drupal-8.3.x.make.yml b/.travis/drupal-8.3.x.make.yml
new file mode 100644
index 0000000..323fff6
--- /dev/null
+++ b/.travis/drupal-8.3.x.make.yml
@@ -0,0 +1,31 @@
+api: 2
+
+core: 8.x
+
+projects:
+  drupal: 8.3.x
+  key_value: 1.x
+  conflict:
+    type: module
+    download:
+      type: git
+      branch: 8.x-1.x
+      url: "https://github.com/drupaldeploy/drupal-conflict.git"
+  multiversion:
+    type: module
+    download:
+      type: git
+      branch: 8.x-1.x
+      url: "https://github.com/dickolsson/drupal-multiversion.git"
+  replication:
+    type: module
+    download:
+      type: git
+      branch: 8.x-1.x
+      url: "https://github.com/relaxedws/drupal-replication.git"
+  workspace:
+    type: module
+    download:
+      type: git
+      branch: 8.x-1.x
+      url: "https://github.com/timmillwood/drupal-workspace.git"
diff --git a/config/install/relaxed.settings.yml b/config/install/relaxed.settings.yml
index 3490a11..251ac58 100644
--- a/config/install/relaxed.settings.yml
+++ b/config/install/relaxed.settings.yml
@@ -1,4 +1 @@
 api_root: "/relaxed"
-authentication:
-  - cookie
-  - basic_auth
diff --git a/config/optional/rest.resource.relaxed.all_dbs.yml b/config/optional/rest.resource.relaxed.all_dbs.yml
new file mode 100644
index 0000000..fb9e80d
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.all_dbs.yml
@@ -0,0 +1,13 @@
+id: relaxed.all_dbs
+plugin_id: 'relaxed:all_dbs'
+granularity: method
+configuration:
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.all_docs.yml b/config/optional/rest.resource.relaxed.all_docs.yml
new file mode 100644
index 0000000..27f0b5f
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.all_docs.yml
@@ -0,0 +1,13 @@
+id: relaxed.all_docs
+plugin_id: 'relaxed:all_docs'
+granularity: method
+configuration:
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.attachment.yml b/config/optional/rest.resource.relaxed.attachment.yml
new file mode 100644
index 0000000..2e042ad
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.attachment.yml
@@ -0,0 +1,36 @@
+id: relaxed.attachment
+plugin_id: 'relaxed:attachment'
+granularity: method
+configuration:
+  HEAD:
+    supported_formats:
+      - json
+      - stream
+      - base64_stream
+    supported_auth:
+      - cookie
+      - basic_auth
+  GET:
+    supported_formats:
+      - json
+      - stream
+      - base64_stream
+    supported_auth:
+      - cookie
+      - basic_auth
+  PUT:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  DELETE:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
+    - file
diff --git a/config/optional/rest.resource.relaxed.bulk_docs.yml b/config/optional/rest.resource.relaxed.bulk_docs.yml
new file mode 100644
index 0000000..5c93fd7
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.bulk_docs.yml
@@ -0,0 +1,13 @@
+id: relaxed.bulk_docs
+plugin_id: 'relaxed:bulk_docs'
+granularity: method
+configuration:
+  POST:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.changes.yml b/config/optional/rest.resource.relaxed.changes.yml
new file mode 100644
index 0000000..0f1ceb6
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.changes.yml
@@ -0,0 +1,13 @@
+id: relaxed.changes
+plugin_id: 'relaxed:changes'
+granularity: method
+configuration:
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.db.yml b/config/optional/rest.resource.relaxed.db.yml
new file mode 100644
index 0000000..c4056cf
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.db.yml
@@ -0,0 +1,37 @@
+id: relaxed.db
+plugin_id: 'relaxed:db'
+granularity: method
+configuration:
+  HEAD:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  POST:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  PUT:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  DELETE:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.doc.yml b/config/optional/rest.resource.relaxed.doc.yml
new file mode 100644
index 0000000..037cf98
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.doc.yml
@@ -0,0 +1,33 @@
+id: relaxed.doc
+plugin_id: 'relaxed:doc'
+granularity: method
+configuration:
+  HEAD:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  GET:
+    supported_formats:
+      - json
+      - mixed
+    supported_auth:
+      - cookie
+      - basic_auth
+  PUT:
+    supported_formats:
+      - json
+      - related
+    supported_auth:
+      - cookie
+      - basic_auth
+  DELETE:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.ensure_full_commit.yml b/config/optional/rest.resource.relaxed.ensure_full_commit.yml
new file mode 100644
index 0000000..19054e9
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.ensure_full_commit.yml
@@ -0,0 +1,13 @@
+id: relaxed.ensure_full_commit
+plugin_id: 'relaxed:ensure_full_commit'
+granularity: method
+configuration:
+  POST:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.local.doc.yml b/config/optional/rest.resource.relaxed.local.doc.yml
new file mode 100644
index 0000000..64b1c1f
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.local.doc.yml
@@ -0,0 +1,25 @@
+id: relaxed.local.doc
+plugin_id: 'relaxed:local:doc'
+granularity: method
+configuration:
+  HEAD:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+  PUT:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.replicate.yml b/config/optional/rest.resource.relaxed.replicate.yml
new file mode 100644
index 0000000..5373342
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.replicate.yml
@@ -0,0 +1,13 @@
+id: relaxed.replicate
+plugin_id: 'relaxed:replicate'
+granularity: method
+configuration:
+  POST:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.revs_diff.yml b/config/optional/rest.resource.relaxed.revs_diff.yml
new file mode 100644
index 0000000..2a2a94d
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.revs_diff.yml
@@ -0,0 +1,13 @@
+id: relaxed.revs_diff
+plugin_id: 'relaxed:revs_diff'
+granularity: method
+configuration:
+  POST:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.root.yml b/config/optional/rest.resource.relaxed.root.yml
new file mode 100644
index 0000000..35094fe
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.root.yml
@@ -0,0 +1,13 @@
+id: relaxed.root
+plugin_id: 'relaxed:root'
+granularity: method
+configuration:
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/optional/rest.resource.relaxed.session.yml b/config/optional/rest.resource.relaxed.session.yml
new file mode 100644
index 0000000..f69f574
--- /dev/null
+++ b/config/optional/rest.resource.relaxed.session.yml
@@ -0,0 +1,13 @@
+id: relaxed.session
+plugin_id: 'relaxed:session'
+granularity: method
+configuration:
+  GET:
+    supported_formats:
+      - json
+    supported_auth:
+      - cookie
+      - basic_auth
+dependencies:
+  module:
+    - basic_auth
diff --git a/config/schema/relaxed.schema.yml b/config/schema/relaxed.schema.yml
index 44087c8..d4d370b 100644
--- a/config/schema/relaxed.schema.yml
+++ b/config/schema/relaxed.schema.yml
@@ -5,12 +5,18 @@ relaxed.settings:
     api_root:
       type: string
       label: 'Absolute base path to the API root, without trailing slash.'
-    authentication:
-      type: sequence
-      label: 'Authentication Providers'
-      sequence:
-        type: string
-        label: 'Authentication provider'
+    username:
+      type: string
+      label: 'Replicator username'
+    password:
+      type: string
+      label: 'Replicator password'
+    encrypt:
+      type: integer
+      label: 'Encrypt sensitive data'
+    encrypt_profile:
+      type: string
+      label: 'Encryption profile to use'
 
 relaxed.endpoint.*:
   type: config_entity
diff --git a/relaxed.info.yml b/relaxed.info.yml
index ae10960..cd2a6ef 100644
--- a/relaxed.info.yml
+++ b/relaxed.info.yml
@@ -4,9 +4,12 @@ description: 'Provides a RESTful API that exposes all content entities over UUID
 package: Web services
 core: 8.x
 dependencies:
+  - rest
   - basic_auth
   - multiversion
   - replication
-  #- system (>=8.4.x)
+  - system (>=8.3.x)
 test_dependencies:
   - workspace
+  - key
+  - encrypt
diff --git a/relaxed.install b/relaxed.install
index a9f3003..4c1ea40 100644
--- a/relaxed.install
+++ b/relaxed.install
@@ -25,3 +25,13 @@ function relaxed_install() {
     ])->save();
   }
 }
+
+/**
+ * Load new rest configuration for RESTful Web Services module.
+ */
+function relaxed_update_8301() {
+  \Drupal::state()->set('relaxed_update_8301_resources', \Drupal::config('rest.settings')->get('resources'));
+  \Drupal::configFactory()->getEditable('rest.settings')
+    ->clear('resources')
+    ->save();
+}
diff --git a/relaxed.module b/relaxed.module
index dce32c9..b78889b 100644
--- a/relaxed.module
+++ b/relaxed.module
@@ -10,11 +10,56 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\relaxed\Entity\Remote;
 use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
 
+/**
+ * Implements hook_entity_insert().
+ */
+function relaxed_entity_insert(EntityInterface $entity) {
+  if ($entity->getEntityTypeId() == 'workspace') {
+    // In some cases (mostly testing) the plugin manager is not yet initialized.
+    // @todo {@link https://www.drupal.org/node/2599864 Fix this.}
+    try {
+      \Drupal::service('plugin.manager.rest')->clearCachedDefinitions();
+    } catch (InvalidArgumentException $e) {
+      watchdog_exception('relaxed', $e);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function relaxed_entity_update(EntityInterface $entity) {
+  if ($entity->getEntityTypeId() == 'workspace') {
+    // In some cases (mostly testing) the plugin manager is not yet initialized.
+    // @todo {@link https://www.drupal.org/node/2599864 Fix this.}
+    try {
+      \Drupal::service('plugin.manager.rest')->clearCachedDefinitions();
+    } catch (InvalidArgumentException $e) {
+      watchdog_exception('relaxed', $e);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function relaxed_entity_delete(EntityInterface $entity) {
+  if ($entity->getEntityTypeId() == 'workspace') {
+    // In some cases (mostly testing) the plugin manager is not yet initialized.
+    // @todo {@link https://www.drupal.org/node/2599864 Fix this.}
+    try {
+      \Drupal::service('plugin.manager.rest')->clearCachedDefinitions();
+    } catch (InvalidArgumentException $e) {
+      watchdog_exception('relaxed', $e);
+    }
+  }
+}
+
 /**
  * Implements hook_entity_access().
  */
 function relaxed_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  /** @var \Drupal\multiversion\MultiversionManagerInterface $multiversion_manager */
+  /** @var \Drupal\multiversion\MultiversionManagerInterface $manager */
   $multiversion_manager = \Drupal::service('multiversion.manager');
   $entity_type = $entity->getEntityType();
   $entity_type_id = $entity_type->id();
@@ -37,7 +82,7 @@ return AccessResult::neutral();
  * Implements hook_entity_create_access().
  */
 function multiversion_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
-  /** @var \Drupal\multiversion\MultiversionManagerInterface $multiversion_manager */
+  /** @var \Drupal\multiversion\MultiversionManagerInterface $manager */
   $multiversion_manager = \Drupal::service('multiversion.manager');
   $bundles_info = \Drupal::service('entity_type.bundle.info')->getAllBundleInfo();
   foreach ($bundles_info as $entity_type_id => $bundles) {
@@ -150,3 +195,10 @@ function relaxed_entity_base_field_info(EntityTypeInterface $entity_type) {
 function relaxed_cron() {
   \Drupal::service('relaxed.remote_pointer')->addAllPointers();
 }
+
+function relaxed_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  if (isset($entity_types['rest_resource_config'])) {
+    $entity_types['rest_resource_config']->setClass('Drupal\relaxed\Entity\RestResourceConfig');
+  }
+}
diff --git a/relaxed.services.yml b/relaxed.services.yml
index fcf03aa..2a51473 100644
--- a/relaxed.services.yml
+++ b/relaxed.services.yml
@@ -1,8 +1,4 @@
 services:
-  logger.channel.relaxed:
-    parent: logger.channel_base
-    arguments: ['relaxed']
-# Param converters
   paramconverter.docid:
     class: Drupal\relaxed\ParamConverter\DocIdConverter
     arguments: ['@entity.manager', '@multiversion.entity_index.uuid', '@multiversion.entity_index.rev', '@multiversion.entity_index.rev.tree']
@@ -18,7 +14,6 @@ services:
     arguments: ['@entity.manager', '@multiversion.entity_index.uuid']
     tags:
       - { name: paramconverter, priority: 20 }
-
   workspace.negotiator.relaxed:
     class: Drupal\relaxed\Workspace\RelaxedWorkspaceNegotiator
     arguments: ['@config.factory']
@@ -28,41 +23,29 @@ services:
       - [setWorkspaceManager, ['@workspace.manager']]
     tags:
       - { name: workspace_negotiator, priority: 200 }
-# Plugin
-  plugin.manager.api_resource:
-    class: Drupal\relaxed\Plugin\ApiResourceManager
-    parent: default_plugin_manager
-  plugin.manager.format_negotiator:
-    class: Drupal\relaxed\Plugin\FormatNegotiatorManager
-    parent: default_plugin_manager
   plugin.manager.remote_check:
     class: Drupal\relaxed\Plugin\RemoteCheckManager
     parent: default_plugin_manager
-
   relaxed.remote_pointer:
     class: Drupal\relaxed\RemotePointer
     arguments: ['@entity_type.manager', '@http_client']
   relaxed.couchdb_replicator:
     class: Drupal\relaxed\CouchdbReplicator
-    arguments: ['@config.factory']
+    arguments: ['@config.factory', '@relaxed.sensitive_data.transformer']
     tags:
       - {name: workspace_replicator, priority: 20}
   relaxed.replicate:
     class: Drupal\relaxed\Replicate\Replicate
-
   relaxed.normalizer.replicate:
     class: Drupal\relaxed\Normalizer\ReplicateNormalizer
     tags:
-      - { name: replication_normalizer, priority: 60 }
+      - { name: normalizer, priority: 10 }
     arguments: ['@relaxed.replicate']
-# Routing
-  relaxed.api_resource.route_generator:
-    class: Drupal\relaxed\Plugin\ApiResourceRouteGenerator
-    arguments: ['@plugin.manager.format_negotiator', '@config.factory', '@logger.channel.relaxed']
-# Subscribers
-  relaxed.resource_routes:
-    class: Drupal\relaxed\Routing\ApiResourceRoutes
-    arguments: ['@plugin.manager.api_resource', '@relaxed.api_resource.route_generator', '@logger.channel.relaxed']
+  relaxed.resource_response.subscriber:
+    class: Drupal\relaxed\EventSubscriber\ResourceResponseSubscriber
     tags:
-      - { name: 'event_subscriber' }
+      - { name: event_subscriber }
     arguments: ['@serializer', '@renderer', '@current_route_match']
+  relaxed.sensitive_data.transformer:
+    class: Drupal\relaxed\SensitiveDataTransformer
+    arguments: ['@config.factory']
diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php
deleted file mode 100644
index 56e1972..0000000
--- a/src/Annotation/ApiResource.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines an API resource plugin annotation.
- *
- * @see \Drupal\relaxed\Plugin\FormatNegotiatorManager
- * @see plugin_api
- *
- * @Annotation
- */
-class ApiResource extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The label of the plugin.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $label;
-
-  /**
-   * The formats allowed by this resource or empty for any format.
-   *
-   * Formats are defined by format negotiator plugins.
-   *
-   * @var array
-   */
-  public $allowed_formats = [];
-
-  /**
-   * The path for the resource.
-   *
-   * @var string
-   */
-  public $path;
-
-  /**
-   * Path parameters.
-   *
-   * @var array
-   */
-  public $parameters;
-
-}
diff --git a/src/Annotation/FormatNegotiator.php b/src/Annotation/FormatNegotiator.php
deleted file mode 100644
index 7c71ed1..0000000
--- a/src/Annotation/FormatNegotiator.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines a format negotiator plugin annotation.
- *
- * @see \Drupal\relaxed\Plugin\FormatNegotiatorManager
- * @see plugin_api
- *
- * @Annotation
- */
-class FormatNegotiator extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The label of the plugin.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $label;
-
-  /**
-   * The formats this negotiator supports.
-   *
-   * @var array
-   */
-  public $formats = [];
-
-  /**
-   * The priority of the negotiator.
-   *
-   * @var int
-   */
-  public $priority = 0;
-
-}
diff --git a/src/Controller/ResourceController.php b/src/Controller/ResourceController.php
index 2033033..5ec5f8e 100644
--- a/src/Controller/ResourceController.php
+++ b/src/Controller/ResourceController.php
@@ -5,91 +5,161 @@ namespace Drupal\relaxed\Controller;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Render\RenderContext;
-use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\HttpMultipart\HttpFoundation\MultipartResponse as HttpFoundationMultipartResponse;
-use Drupal\relaxed\Plugin\ApiResourceInterface;
-use Drupal\relaxed\Plugin\ApiResourceManagerInterface;
-use Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface;
-use Drupal\replication\ProcessFileAttachment;
+use Drupal\relaxed\HttpMultipart\HttpFoundation\MultipartResponse;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\AcceptHeader;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
-use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
 use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Serializer\Serializer;
 
-/**
- * Controller to handle all requests for API resource routes.
- */
-class ResourceController implements ContainerInjectionInterface {
+class ResourceController implements ContainerAwareInterface, ContainerInjectionInterface {
+
+  use ContainerAwareTrait;
+
+  /**
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected $serializer;
 
   /**
-   * The default format.
+   * @var \Symfony\Component\HttpFoundation\Request $request
    */
-  const DEFAULT_FORMAT = 'json';
+  protected $request;
 
   /**
    * The resource configuration storage.
    *
-   * @var \Drupal\relaxed\Plugin\ApiResourceManagerInterface
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $resourceStorage;
+
+  /**
+   * Creates a new RequestHandler instance.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The resource configuration storage.
+   */
+  public function __construct(EntityStorageInterface $entity_storage) {
+    $this->resourceStorage = $entity_storage;
+  }
+
+  /**
+   * {@inheritdoc}
    */
-  protected $resourceManager;
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('entity_type.manager')->getStorage('rest_resource_config'));
+  }
 
   /**
-   * @var \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface
+   * @return \Symfony\Component\DependencyInjection\ContainerInterface
    */
-  protected $negotiatorManager;
+  protected function container() {
+    return \Drupal::getContainer();
+  }
 
   /**
-   * @var \Drupal\replication\ProcessFileAttachment
+   * @return \Symfony\Component\Serializer\SerializerInterface
    */
-  protected $attachment;
+  protected function serializer() {
+    if (!$this->serializer) {
+      $this->serializer = $this->container()->get('serializer');
+    }
+    return $this->serializer;
+  }
 
   /**
-   * @var \Drupal\Core\Render\RendererInterface
+   * @return string
    */
-  protected $renderer;
+  protected function getMethod() {
+    return strtolower($this->request->getMethod());
+  }
 
   /**
-   * Creates a new RequestHandler instance.
+   * @return string
+   */
+  protected function getFormat() {
+    if (!$format = $this->request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format')) {
+      return $this->getResource()->isAttachment() ? 'stream' : 'json';
+    }
+    return $format;
+  }
+
+  /**
+   * @return \Drupal\relaxed\Plugin\rest\resource\RelaxedResourceInterface
+   */
+  protected function getResource() {
+    $plugin_id = $this->request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getDefault('_plugin');
+    return $this->container()
+      ->get('plugin.manager.rest')
+      ->getInstance(['id' => $plugin_id]);
+  }
+
+  /**
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *
-   * @param \Drupal\relaxed\Plugin\ApiResourceManagerInterface $resource_manager
-   *   The API resource manager.
-   * @param \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface $negotiator_manager
-   *   The format negotiator manager.
-   * @param \Drupal\replication\ProcessFileAttachment $attachment
-   *   The file attachment processor.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer.
+   * @return array
    */
-  public function __construct(ApiResourceManagerInterface $resource_manager, FormatNegotiatorManagerInterface $negotiator_manager, ProcessFileAttachment $attachment, RendererInterface $renderer) {
-    $this->resourceManager = $resource_manager;
-    $this->negotiatorManager = $negotiator_manager;
-    $this->attachment = $attachment;
-    $this->renderer = $renderer;
+  protected function getParameters(RouteMatchInterface $route_match) {
+    $parameters = [];
+    foreach ($route_match->getParameters() as $key => $parameter) {
+      // We don't want private parameters.
+      if ($key{0} !== '_') {
+        $parameters[] = $parameter;
+      }
+    }
+    return $parameters;
   }
 
   /**
-   * {@inheritdoc}
+   * Helper method for returning error responses.
+   *
+   * @todo {@link https://www.drupal.org/node/2599912 Improve to handle error and reason messages more generically.}
    */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('plugin.manager.api_resource'),
-      $container->get('plugin.manager.format_negotiator'),
-      $container->get('replication.process_file_attachment'),
-      $container->get('renderer')
-    );
+  public function errorResponse(\Exception $e) {
+    // Default to 400 Bad Request.
+    $status = 400;
+    $error = 'bad_request';
+    $reason = $e->getMessage();
+
+    if ($e instanceof HttpExceptionInterface) {
+      $status = $e->getStatusCode();
+    }
+
+    if ($e instanceof UnauthorizedHttpException || $e instanceof AccessDeniedHttpException) {
+      $error = 'unauthorized';
+    }
+    elseif ($e instanceof NotFoundHttpException) {
+      $error = 'not_found';
+    }
+    elseif ($e instanceof ConflictHttpException) {
+      $error = 'conflict';
+    }
+    elseif ($e instanceof PreconditionFailedHttpException) {
+      $error = 'file_exists';
+    }
+
+    $content = '';
+    $headers = [];
+    // We shouldn't respond with content for HEAD requests.
+    if ($this->request->getMethod() != 'HEAD') {
+      $format = $this->getFormat();
+      $headers = ['Content-Type' => $this->request->getMimeType($format)];
+      $data = ['error' => $error, 'reason' => $reason];
+      $content = $this->serializer()->serialize($data, $format);
+    }
+    watchdog_exception('Relaxed', $e);
+    return new Response($content, $status, $headers);
   }
 
   /**
@@ -99,37 +169,27 @@ class ResourceController implements ContainerInjectionInterface {
    * @return \Symfony\Component\HttpFoundation\Response
    */
   public function handle(RouteMatchInterface $route_match, Request $request) {
-    $route = $route_match->getRouteObject();
-    $method = strtolower($request->getMethod());
-
-    $api_resource_id = $route->getDefault('_api_resource');
-    $api_resource = $this->resourceManager->createInstance($api_resource_id);
+    $this->request = $request;
 
-    $content_type_format = $this->getContentTypeFormat($route, $request, $api_resource);
+    $method = $this->getMethod();
+    $format = $this->getFormat();
+    $resource = $this->getResource();
 
-    // Select the format negotiator for the request data.
-    $negotiator = $this->negotiatorManager->select($content_type_format, $method, 'request');
-    $serializer = $negotiator->serializer($content_type_format, $method, 'request');
-
-    $content = $request->getContent();
+    $content = $this->request->getContent();
     $parameters = $this->getParameters($route_match);
     $render_contexts = [];
 
     // @todo {@link https://www.drupal.org/node/2600500 Check if this is safe.}
-    $query = $request->query->all();
-    $context = [
-      'query' => $query,
-      'api_resource_id' => $api_resource_id,
-    ];
-
+    $query = $this->request->query->all();
+    $resource_config_id = $route_match->getRouteObject()->getDefault('_rest_resource_config');
+    $context = ['query' => $query, 'resource_id' => $resource_config_id];
     $entity = NULL;
-    $definition = $api_resource->getPluginDefinition();
-
+    $definition = $resource->getPluginDefinition();
     if (!empty($content)) {
       try {
         $class = isset($definition['serialization_class'][$method]) ? $definition['serialization_class'][$method] : $definition['serialization_class']['canonical'];
 
-        // If we have a workspace parameter, pass it to the serializer for denormalization.
+        // If we have a workspace parameter, pass it to the deserializer.
         foreach ($parameters as $parameter) {
           if ($parameter instanceof WorkspaceInterface) {
             $context['workspace'] = $parameter;
@@ -137,257 +197,119 @@ class ResourceController implements ContainerInjectionInterface {
           }
         }
 
-        $entity = $serializer->deserialize($content, $class, $content_type_format, $context);
+        $entity = $this->serializer()->deserialize($content, $class, $format, $context);
       }
       catch (\Exception $e) {
-        return $this->errorResponse($e, $content_type_format, $serializer, $request);
+        return $this->errorResponse($e);
       }
     }
 
     try {
       $render_context = new RenderContext();
-      /** @var \Drupal\relaxed\Http\ApiResourceResponse $response */
-      $response = $this->renderer->executeInRenderContext($render_context, function() use ($api_resource, $method, $parameters, $entity, $request) {
-        return call_user_func_array([$api_resource, $method], array_merge($parameters, [$entity, $request]));
+      /** @var \Drupal\rest\ResourceResponse $response */
+      $response = $this->container->get('renderer')->executeInRenderContext($render_context, function() use ($resource, $method, $parameters, $entity, $request) {
+        return call_user_func_array([$resource, $method], array_merge($parameters, [$entity, $request]));
       });
-
       if (!$render_context->isEmpty()) {
         $render_contexts[] = $render_context->pop();
       }
     }
     catch (\Exception $e) {
-      return $this->errorResponse($e, $content_type_format, $serializer, $request);
+      return $this->errorResponse($e);
     }
 
-    // Select the format negotiator for the response data.
-    $response_format = $this->getResponseFormat($route, $request, $api_resource);
-
-    $negotiator = $this->negotiatorManager->select($response_format, $method, 'response');
-    $serializer = $negotiator->serializer($response_format, $method, 'response');
-
-    // @todo This is not nice, it's hacky. Find a nicer way to get response
-    // formats based on the chosen negotiator. We might have to switch to one
-    // format per negotiator.
-    // If the format for the response is not in the chosen negotiator, use the
-    // first one from the definition.
-    $negotiator_formats = $negotiator->getPluginDefinition()['formats'];
-    if (!in_array($response_format, $negotiator_formats, TRUE)) {
-      // Use the first from the chosen negotiator.
-      $response_format = reset($negotiator_formats);
-    }
+    $response_format = (in_array($request->getMethod(), ['GET', 'HEAD']) && $format == 'stream')
+      ? 'stream'
+      : 'json';
 
-    $responses = ($response instanceof HttpFoundationMultipartResponse) ? $response->getParts() : [$response];
+    $responses = ($response instanceof MultipartResponse) ? $response->getParts() : [$response];
 
     $render_contexts = [];
-
+    $serializer = $this->serializer();
     foreach ($responses as $response_part) {
       if ($response_data = $response_part->getResponseData()) {
         // Collect bubbleable metadata in a render context.
         $render_context = new RenderContext();
-        $response_output = $this->renderer->executeInRenderContext($render_context, function() use ($serializer, $response_data, $response_format, $context) {
+        $response_output = $this->container->get('renderer')->executeInRenderContext($render_context, function() use ($serializer, $response_data, $response_format, $context) {
           return $serializer->serialize($response_data, $response_format, $context);
         });
-
         if (!$render_context->isEmpty()) {
           $render_contexts[] = $render_context->pop();
         }
-
         $response_part->setContent($response_output);
       }
-
-      if (!$response_part->headers->has('Content-Type')) {
+      if (!$response_part->headers->get('Content-Type')) {
         $response_part->headers->set('Content-Type', $request->getMimeType($response_format));
       }
     }
 
-    if ($method !== 'head') {
+    if ($request->getMethod() !== 'HEAD') {
       $response->headers->set('Content-Length', strlen($response->getContent()));
     }
 
+    /** @var \Drupal\rest\RestResourceConfigInterface $resource_config */
+    $resource_config = $this->resourceStorage->load($resource_config_id);
     if ($response instanceof CacheableResponseInterface) {
-      // Add API resource and format negotiator as dependencies.
-      $response->addCacheableDependency($api_resource);
-      $response->addCacheableDependency($negotiator);
-
-      $cacheable_dependencies = [];
+      // Add rest config's cache tags.
+      $response->addCacheableDependency($resource_config);
+    }
 
-      foreach ($render_contexts as $render_context) {
-        $cacheable_dependencies[] = $render_context;
+    $cacheable_dependencies = [];
+    foreach ($render_contexts as $render_context) {
+      $cacheable_dependencies[] = $render_context;
+    }
+    foreach ($parameters as $parameter) {
+      if (is_array($parameter)) {
+        array_merge($cacheable_dependencies, $parameter);
       }
-
-      foreach ($parameters as $parameter) {
-        if (is_array($parameter)) {
-          array_merge($cacheable_dependencies, $parameter);
-        }
-        else {
-          $cacheable_dependencies[] = $parameter;
-        }
+      else {
+        $cacheable_dependencies[] = $parameter;
       }
-
-      $cacheable_metadata = new CacheableMetadata();
-      $cacheable_dependencies[] = $cacheable_metadata->setCacheContexts(['url', 'request_format', 'headers:If-None-Match', 'headers:Content-Type', 'headers:Accept']);
-
-      $response->addCacheableDependency($cacheable_dependencies);
     }
+    $cacheable_metadata = new CacheableMetadata();
+    $cacheable_dependencies[] = $cacheable_metadata->setCacheContexts(['url', 'request_format', 'headers:If-None-Match', 'headers:Content-Type', 'headers:Accept']);
+    $this->addCacheableDependency($response, $cacheable_dependencies);
 
     return $response;
   }
 
   /**
-   * Gets the response format.
-   *
-   * This uses the content type format if an actual format is no specified.
+   * Generates a CSRF protecting session token.
    *
-   * @return string
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response object.
    */
-  protected function getResponseFormat(Route $route, Request $request, ApiResourceInterface $api_resource) {
-    $api_resource_formats = $api_resource->getAllowedFormats();
-
-    $acceptable_response_formats = $this->getAcceptableResponseFormatsFromRequestHeaders($request);
-    $acceptable_route_response_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
-
-    if ($acceptable_response_formats) {
-      // If there are required formats enforced, intersect that with the acceptable formats from the accept header.
-      if ($acceptable_route_response_formats) {
-        $acceptable_response_formats = array_intersect($acceptable_response_formats, $acceptable_route_response_formats);
-      }
-    }
-    // Otherwise, use route ones.
-    else {
-      $acceptable_response_formats = $acceptable_route_response_formats;
-    }
-
-    $acceptable_formats = !empty($api_resource_formats) ? $api_resource_formats : $acceptable_response_formats;
-
-    $requested_format = $request->getRequestFormat();
-    $content_type_format = $request->getContentType();
-
-    // If an acceptable format is requested, then use that. Otherwise, including
-    // and particularly when the client forgot to specify a format, then use
-    // heuristics to select the format that is most likely expected.
-    // Special case mixed here. We can't use this as a response format for
-    // serialization.
-    if (!in_array($requested_format, ['mixed', 'related'], TRUE) && (empty($acceptable_formats) || in_array($requested_format, $acceptable_formats))) {
-      return $requested_format;
-    }
-    // If a request body is present, then use the format corresponding to the
-    // request body's Content-Type for the response, if it's an acceptable
-    // format for the request.
-    elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_formats)) {
-      return $content_type_format;
-    }
-    // Otherwise, use the first acceptable format.
-    elseif (!empty($acceptable_formats)) {
-      return reset($acceptable_formats);
-    }
-    // Return the default format.
-    else {
-      return $acceptable_response_formats ? reset($acceptable_response_formats) : static::DEFAULT_FORMAT;
-    }
+  public function csrfToken() {
+    return new Response(\Drupal::csrfToken()->get('rest'), 200, ['Content-Type' => 'text/plain']);
   }
 
   /**
-   * Gets the content type format.
+   * Check if a string is a valid json.
    *
-   * @return string
-   */
-  protected function getContentTypeFormat(Route $route, Request $request, ApiResourceInterface $api_resource) {
-    $api_resource_formats = $api_resource->getAllowedFormats();
-    $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
-    $acceptable_formats = !empty($api_resource_formats) ? $api_resource_formats : $acceptable_content_type_formats;
-
-    $content_type_format = $request->getContentType();
-
-    // If a request body is present, then use the format corresponding to the
-    // request body's Content-Type for the response, if it's an acceptable
-    // format for the request.
-    if (in_array($content_type_format, $acceptable_formats)) {
-      return $content_type_format;
-    }
-    // Otherwise, use the first acceptable format.
-    elseif (!empty($acceptable_formats)) {
-      return reset($acceptable_formats);
-    }
-    // Return the default format.
-    else {
-      return static::DEFAULT_FORMAT;
-    }
-  }
-
-  /**
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   * @param $string
    *
-   * @return array
+   * @return bool
    */
-  protected function getParameters(RouteMatchInterface $route_match) {
-    $parameters = [];
-    foreach ($route_match->getParameters() as $key => $parameter) {
-      // We don't want private parameters.
-      if ($key{0} !== '_') {
-        $parameters[] = $parameter;
-      }
-    }
-    return $parameters;
+  protected function isValidJson($string) {
+    json_decode($string);
+    return (json_last_error() == JSON_ERROR_NONE);
   }
 
   /**
-   * @param Request $request
+   * Adds cacheable dependencies.
    *
-   * @return array
+   * @param \Drupal\Core\Cache\CacheableResponseInterface
+   * @param $parameters
    */
-  protected function getAcceptableResponseFormatsFromRequestHeaders(Request $request) {
-    $acceptable_content_types = array_keys(AcceptHeader::fromString($request->headers->get('X-Relaxed-Document-Accept'))->all());
-
-    return array_unique(array_filter(array_map(function ($content_type) use ($request) {
-      return $request->getFormat($content_type);
-    }, $acceptable_content_types)));
-  }
-
-  /**
-   * Helper method for returning error responses.
-   *
-   * @todo {@link https://www.drupal.org/node/2599912 Improve to handle error and reason messages more generically.}
-   */
-  protected function errorResponse(\Exception $e, $format, Serializer $serializer, Request $request) {
-    // Default to 400 Bad Request.
-    $status = 400;
-    $error = 'bad_request';
-    $reason = $e->getMessage();
-    $headers = [];
-
-    if ($e instanceof HttpExceptionInterface) {
-      $status = $e->getStatusCode();
-      // Use any headers passed from the HTTP exception.
-      $headers = $e->getHeaders();
-    }
-
-    if ($e instanceof UnauthorizedHttpException || $e instanceof AccessDeniedHttpException) {
-      $error = 'unauthorized';
-    }
-    elseif ($e instanceof NotFoundHttpException) {
-      $error = 'not_found';
-    }
-    elseif ($e instanceof ConflictHttpException) {
-      $error = 'conflict';
-    }
-    elseif ($e instanceof PreconditionFailedHttpException) {
-      $error = 'file_exists';
+  protected function addCacheableDependency(CacheableResponseInterface $response, $parameters) {
+    if (is_array($parameters)) {
+      foreach ($parameters as $parameter) {
+        $response->addCacheableDependency($parameter);
+      }
     }
-
-    // Always try and set the content type for the response.
-    $headers['Content-Type'] = $request->getMimeType($format);
-
-    $content = '';
-
-    // We shouldn't respond with content for HEAD requests.
-    if ($request->getMethod() != 'HEAD') {
-      $data = ['error' => $error, 'reason' => $reason];
-      $content = $serializer->serialize($data, $format);
+    else {
+      $response->addCacheableDependency($parameters);
     }
-
-    watchdog_exception('relaxed', $e);
-
-    return new Response($content, $status, $headers);
   }
 
 }
diff --git a/src/CouchdbReplicator.php b/src/CouchdbReplicator.php
index deb3d1e..8b82028 100644
--- a/src/CouchdbReplicator.php
+++ b/src/CouchdbReplicator.php
@@ -6,6 +6,7 @@ use Doctrine\CouchDB\CouchDBClient;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Url;
 use Drupal\multiversion\Entity\WorkspaceInterface;
+use Drupal\relaxed\SensitiveDataTransformer;
 use Drupal\relaxed\Entity\RemoteInterface;
 use Drupal\replication\Entity\ReplicationLog;
 use Drupal\replication\ReplicationTask\ReplicationTaskInterface;
@@ -18,10 +19,21 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
 
 class CouchdbReplicator implements ReplicatorInterface{
 
+  /**
+   * Relaxed configuration settings.
+   */
   protected $relaxedSettings;
 
-  public function __construct(ConfigFactoryInterface $config_factory) {
+  /**
+   * Relaxed sensitive data transformer service.
+   *
+   * @var Drupal\relaxed\SensitiveDataTransformer
+   */
+  protected $transformer;
+
+  public function __construct(ConfigFactoryInterface $config_factory, SensitiveDataTransformer $transformer) {
     $this->relaxedSettings = $config_factory->get('relaxed.settings');
+    $this->transformer = $transformer;
   }
 
   /**
@@ -67,8 +79,12 @@ class CouchdbReplicator implements ReplicatorInterface{
           ->loadByProperties(['uuid' => $replication_log_id]);
         $replication_log = reset($replication_logs);
         $since = 0;
-        if (!empty($replication_log) && $replication_log->get('ok')->value == TRUE) {
-          $since = $replication_log->getSourceLastSeq() ?: $since;
+        if (!empty($replication_log) && $replication_log->get('ok')->value == TRUE && $replication_log_history = $replication_log->getHistory()) {
+          $dw = $replication_log_history[0]['docs_written'];
+          $mf = $replication_log_history[0]['missing_found'];
+          if ($dw !== NULL && $mf !== NULL && $dw == $mf) {
+            $since = $replication_log->getSourceLastSeq() ?: $since;
+          }
         }
         $couchdb_task->setSinceSeq($since);
       }
@@ -112,7 +128,7 @@ class CouchdbReplicator implements ReplicatorInterface{
       $uri = new Uri($url);
       $uri = $uri->withUserInfo(
         $this->relaxedSettings->get('username'),
-        $this->relaxedSettings->get('password')
+        $this->transformer->get($this->relaxedSettings->get('password'))
       );
     }
 
diff --git a/src/Entity/Remote.php b/src/Entity/Remote.php
index ac70808..5947666 100644
--- a/src/Entity/Remote.php
+++ b/src/Entity/Remote.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\relaxed\Entity;
 
+use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\encrypt\Entity\EncryptionProfile;
 use GuzzleHttp\Psr7\Uri;
 
 /**
@@ -73,7 +75,8 @@ class Remote extends ConfigEntityBase implements RemoteInterface {
   protected $uri;
 
   public function uri() {
-    return new Uri(base64_decode($this->uri));
+    $uri = \Drupal::service('relaxed.sensitive_data.transformer')->get($this->uri);
+    return new Uri($uri);
   }
 
   public function withoutUserInfo() {
diff --git a/src/Entity/RestResourceConfig.php b/src/Entity/RestResourceConfig.php
new file mode 100644
index 0000000..8cc8f85
--- /dev/null
+++ b/src/Entity/RestResourceConfig.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\relaxed\Entity;
+
+use Drupal\rest\Entity\RestResourceConfig as CoreRestResourceConfig;
+
+class RestResourceConfig extends CoreRestResourceConfig {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function normalizeRestMethod($method) {
+    if (substr($this->get('plugin_id'), 0, 7) === 'relaxed') {
+      $valid_methods = ['GET', 'POST', 'PATCH', 'DELETE', 'PUT', 'HEAD'];
+      $normalised_method = strtoupper($method);
+      if (!in_array($normalised_method, $valid_methods)) {
+        throw new \InvalidArgumentException('The method is not supported.');
+      }
+      return $normalised_method;
+    } 
+    else {
+      return parent::normalizeRestMethod($method);
+    }
+  }
+
+}
diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php
new file mode 100644
index 0000000..3ff5eb0
--- /dev/null
+++ b/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\relaxed\EventSubscriber;
+
+use Drupal\rest\ResourceResponseInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Drupal\rest\EventSubscriber\ResourceResponseSubscriber as CoreResourceResponseSubscriber;
+
+class ResourceResponseSubscriber extends CoreResourceResponseSubscriber {
+
+  /**
+   * Serializes ResourceResponse relaxed response's data.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    if (!$response instanceof ResourceResponseInterface) {
+      return;
+    }
+
+    if ($this->isRelaxedRoute()) {
+      $event->setResponse($this->flattenResponse($response));
+    }
+  }
+
+  protected function isRelaxedRoute() {
+    return (substr($this->routeMatch->getRouteObject()->getDefault('_rest_resource_config'), 0, strlen('relaxed')) === 'relaxed');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run shortly before \Drupal\rest\EventSubscriber\ResourceResponseSubscriber.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', static::determineOnResponsePriority()];
+    return $events;
+  }
+
+  /**
+   * Determines correct response priority based on the Drupal minor version.
+   *
+   * @return int
+   */
+  public static function determineOnResponsePriority() {
+    // Get the minor version only from the \Drupal::VERSION string.
+    $minor_version = substr(\Drupal::VERSION, 0, 3);
+
+    // In versions before 8.4 the rest ResourceResponseSubscriber had a
+    // priority of 5. In https://www.drupal.org/node/2827797 it got
+    // increased to 128. So 8.4 needs to have a priority higher than that.
+    if (version_compare($minor_version, '8.4', '<')) {
+      return 6;
+    }
+
+    return 129;
+  }
+
+}
diff --git a/src/Form/RemoteForm.php b/src/Form/RemoteForm.php
index e7598b4..b7caea3 100644
--- a/src/Form/RemoteForm.php
+++ b/src/Form/RemoteForm.php
@@ -10,8 +10,9 @@ use GuzzleHttp\Psr7\Uri;
  * Class RemoteForm.
  */
 class RemoteForm extends EntityForm {
+
   /**
-   * @inheritDoc
+   * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     if (!\Drupal::moduleHandler()->moduleExists('workspace')) {
@@ -86,7 +87,7 @@ class RemoteForm extends EntityForm {
       $form_state->getValue('username'),
       $form_state->getValue('password')
     );
-    $encoded = base64_encode($uri);
+    $encoded = \Drupal::service('relaxed.sensitive_data.transformer')->set($uri);
     $remote->set('uri', $encoded);
     $status = $remote->save();
 
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index d9877e5..4e0f728 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -2,11 +2,9 @@
 
 namespace Drupal\relaxed\Form;
 
-use Drupal\Core\Authentication\AuthenticationCollectorInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Url;
 
 /**
  * Class SettingsForm.
@@ -15,30 +13,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  */
 class SettingsForm extends ConfigFormBase {
 
-  protected $authenticationCollector;
-
-  /**
-   * Constructs a SettingsForm object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
-   */
-  public function __construct(ConfigFactoryInterface $config_factory, AuthenticationCollectorInterface $collector) {
-    parent::__construct($config_factory);
-
-    $this->authenticationCollector = $collector;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('config.factory'),
-      $container->get('authentication_collector')
-    );
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -61,8 +35,10 @@ class SettingsForm extends ConfigFormBase {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $config = $this->config('relaxed.settings');
 
-    $authentication_providers = array_keys($this->authenticationCollector->getSortedProviders());
-    $authentication_providers = array_combine($authentication_providers, $authentication_providers);
+    $encrypt = \Drupal::service('module_handler')->moduleExists('encrypt');
+    if ($encrypt) {
+      $encrypt_profiles = \Drupal::service('encrypt.encryption_profile.manager')->getEncryptionProfileNamesAsOptions();
+    }
 
     $form['api_root'] = [
       '#type' => 'textfield',
@@ -74,14 +50,6 @@ class SettingsForm extends ConfigFormBase {
       '#required' => TRUE,
     ];
 
-    $form['authentication'] = [
-      '#title' => $this->t('Authentication providers'),
-      '#description' => $this->t('The allowed authentication providers for relaxed routes.'),
-      '#type' => 'checkboxes',
-      '#options' => $authentication_providers,
-      '#default_value' => $config->get('authentication'),
-    ];
-
     $form['creds'] = [
       '#type' => 'fieldset',
       '#title' => t('Default replicator credentials'),
@@ -102,6 +70,48 @@ class SettingsForm extends ConfigFormBase {
       '#maxlength' => 128,
     ];
 
+    $form['encryption'] = [
+      '#type' => 'details',
+      '#title' => t('Encryption'),
+      '#description' => t('Encrypt all sensitive data using the Encrypt module including passwords and URIs.'),
+      '#open' => $encrypt,
+    ];
+    $form['encryption']['encrypt'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Encrypt all sensitive data'),
+      '#default_value' => $config->get('encrypt'),
+      '#disabled' => !$encrypt || empty($encrypt_profiles),
+    ];
+    if (!empty($encrypt_profiles)) {
+      $form['encryption']['encrypt_profile'] = [
+        '#type' => 'select',
+        '#title' => t('Encryption profile'),
+        '#options' => $encrypt_profiles,
+        '#default_value' => $config->get('encrypt_profile'),
+        '#states' => [
+          'visible' => [
+            ':input[name="encrypt"]' => ['checked' => TRUE],
+          ],
+          'required' => [
+            ':input[name="encrypt"]' => ['checked' => TRUE],
+          ],
+          'disabled' => [
+            ':input[name="encrypt"]' => ['checked' => FALSE],
+          ],
+        ],
+        '#required' => FALSE,
+      ];
+    }
+    elseif ($encrypt) {
+      $args = [
+        '@encryption_profile' => \Drupal::l('encryption profile', Url::fromRoute('entity.encryption_profile.collection')),
+      ];
+      $form['encryption']['help'] = [
+        '#type' => 'item',
+        '#description' => t('An @encryption_profile must be created before encryption can be enabled.', $args),
+      ];
+    }
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -119,13 +129,36 @@ class SettingsForm extends ConfigFormBase {
     $config = $this->config('relaxed.settings');
     $username = $form_state->getValue('username');
     $password = $form_state->getValue('password');
+    $encrypt = $form_state->getValue('encrypt');
+    $encrypt_profile = $form_state->getValue('encrypt_profile');
 
-    if ($username && !($password || $config->get('password'))) {
-      $form_state->setErrorByName('password', 'When setting a username you must set a password.');
-    }
     if (!$username && $password) {
-      $form_state->setErrorByName('username', 'When setting a password you must set a username.');
+      $form_state->setErrorByName('username', 'A username must be entered when setting a password.');
+    }
+    if ($username && !$password) {
+      $form_state->setErrorByName('password', 'A password must be entered when setting a username.');
+    }
+
+    if ($encrypt && !$encrypt_profile) {
+      $form_state->setErrorByName('encrypt_profile', 'An encryption profile must be selected when enabling encryption.');
     }
+
+    // Check if encryption settings have changed.
+    if ($encrypt != $config->get('encrypt') || $encrypt_profile != $config->get('encrypt_profile')) {
+      // Force the credentials to be entered if the encryption settings change.
+      if (!$username) {
+        $form_state->setErrorByName('username', 'Credentials must be entered when changing encryption settings.');
+      }
+      else {
+        // Warn the user when altering encryption settings to update remote
+        // settings.
+        $args = [
+          '@remotes' => \Drupal::l('relaxed remotes', Url::fromRoute('entity.remote.collection')),
+        ];
+        drupal_set_message(t('All @remotes must be updated when altering the encryption settings.', $args), 'warning');
+      }
+    }
+
   }
 
   /**
@@ -134,14 +167,21 @@ class SettingsForm extends ConfigFormBase {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);
 
+    // Get configuration and update it from form.
     $config = $this->config('relaxed.settings');
-
     $config
       ->set('api_root', $form_state->getValue('api_root'))
-      ->set('authentication', array_keys(array_filter($form_state->getValue('authentication'))))
       ->set('username', $form_state->getValue('username'))
-      ->set('password', $form_state->getValue('password'))
+      ->set('encrypt', $form_state->getValue('encrypt'))
+      ->set('encrypt_profile', $form_state->getValue('encrypt_profile'))
       ->save();
+
+    // If a password is entered then update it.
+    $password = $form_state->getValue('password');
+    if ($password) {
+      $password = \Drupal::service('relaxed.sensitive_data.transformer')->set($password);
+      $config->set('password', $password)->save();
+    }
   }
 
 }
diff --git a/src/Http/ApiResourceResponse.php b/src/Http/ApiResourceResponse.php
deleted file mode 100644
index b0940cb..0000000
--- a/src/Http/ApiResourceResponse.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Http;
-
-use Drupal\Core\Cache\CacheableResponseInterface;
-use Drupal\Core\Cache\CacheableResponseTrait;
-use Symfony\Component\HttpFoundation\Response;
-
-/**
- * Contains data for serialization before sending the response.
- *
- * We do not want to abuse the $content property on the Response class to store
- * our response data. $content implies that the provided data must either be a
- * string or an object with a __toString() method, which is not a requirement
- * for data used here.
- *
- * @see \Drupal\rest\ModifiedResourceResponse
- */
-class ApiResourceResponse extends Response implements CacheableResponseInterface {
-
-  use CacheableResponseTrait;
-
-  /**
-   * Response data that should be serialized.
-   *
-   * @var mixed
-   */
-  protected $responseData;
-
-  /**
-   * Constructor for ResourceResponse objects.
-   *
-   * @param mixed $data
-   *   Response data that should be serialized.
-   * @param int $status
-   *   The response status code.
-   * @param array $headers
-   *   An array of response headers.
-   */
-  public function __construct($data = NULL, $status = 200, $headers = []) {
-    $this->responseData = $data;
-    parent::__construct('', $status, $headers);
-  }
-
-  /**
-   * Returns response data that should be serialized.
-   *
-   * @return mixed
-   *   Response data that should be serialized.
-   */
-  public function getResponseData() {
-    return $this->responseData;
-  }
-
-}
diff --git a/src/HttpMultipart/HttpFoundation/MultipartResponse.php b/src/HttpMultipart/HttpFoundation/MultipartResponse.php
index 737974c..783c803 100644
--- a/src/HttpMultipart/HttpFoundation/MultipartResponse.php
+++ b/src/HttpMultipart/HttpFoundation/MultipartResponse.php
@@ -2,11 +2,11 @@
 
 namespace Drupal\relaxed\HttpMultipart\HttpFoundation;
 
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 
-class MultipartResponse extends ApiResourceResponse {
+class MultipartResponse extends ResourceResponse {
   /**
    * @var string
    */
@@ -20,7 +20,7 @@ class MultipartResponse extends ApiResourceResponse {
   /**
    * @var Response[]
    */
-  protected $parts = [];
+  protected $parts;
 
   /**
    * Constructor.
@@ -28,38 +28,30 @@ class MultipartResponse extends ApiResourceResponse {
   public function __construct(array $parts = NULL, $status = 200, $headers = [], $subtype = NULL) {
     parent::__construct(NULL, $status, $headers);
 
-    if ($parts) {
-      $this->setParts($parts);
-    }
-
     $this->subtype = $subtype ?: 'mixed';
     $this->boundary = md5(microtime());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare(Request $request) {
-    $this->headers->set('Content-Type', sprintf('multipart/%s; boundary="%s"', $this->subtype, $this->boundary));
-    $this->headers->set('Transfer-Encoding', 'chunked');
-
-    // Prepare the response content from the parts.
-    $parts = $this->getParts();
 
-    if ($parts) {
+    if (NULL !== $parts) {
+      $this->setParts($parts);
       $content = '';
-
       foreach ($this->getParts() as $part) {
-        $content .= sprintf("--%s\r\n", $this->boundary);
-        $content .= sprintf("Content-Type: %s\r\n\r\n", $part->headers->get('Content-Type'));
-        $content .= $part->getContent();
+        $content .= "--{$this->boundary}\r\n";
+        $content .= "Content-Type: {$part->headers->get('Content-Type')}\r\n\r\n";
+        $content .= \Drupal::service('serializer')
+          ->serialize($part->getResponseData(), 'json');
         $content .= "\r\n";
       }
-
-      $content .= sprintf("--%s--", $this->boundary);
-
+      $content .= "--{$this->boundary}--";
       $this->setContent($content);
     }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepare(Request $request) {
+    $this->headers->set('Content-Type', "multipart/{$this->subtype}; boundary=\"{$this->boundary}\"");
+    $this->headers->set('Transfer-Encoding', 'chunked');
 
     return parent::prepare($request);
   }
diff --git a/src/ParamConverter/DbConverter.php b/src/ParamConverter/DbConverter.php
index f276a4b..792e336 100644
--- a/src/ParamConverter/DbConverter.php
+++ b/src/ParamConverter/DbConverter.php
@@ -40,7 +40,7 @@ class DbConverter implements ParamConverterInterface {
     $workspace = $this->workspaceManager->loadByMachineName($machine_name);
     if (!$workspace) {
       $methods = $defaults['_route_object']->getMethods();
-      if (in_array('PUT', $methods) && $defaults['_api_resource'] == 'db') {
+      if (in_array('PUT', $methods) && $defaults['_plugin'] == 'relaxed:db') {
         $workspace_types = WorkspaceType::loadMultiple();
         $workspace_type = reset($workspace_types);
         $workspace = Workspace::create([
diff --git a/src/Plugin/ApiResource/ApiResourceBase.php b/src/Plugin/ApiResource/ApiResourceBase.php
deleted file mode 100644
index 9652fca..0000000
--- a/src/Plugin/ApiResource/ApiResourceBase.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin\ApiResource;
-
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Plugin\PluginBase;
-use Drupal\relaxed\Plugin\ApiResourceInterface;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Base API resource plugin.
- */
-abstract class ApiResourceBase extends PluginBase implements ApiResourceInterface {
-
-  /**
-   * @var array
-   */
-  protected $cacheTags = [];
-
-  /**
-   * Validates a content entity.
-   *
-   * @param ContentEntityInterface $entity
-   */
-  protected function validate(ContentEntityInterface $entity) {
-    $violations = $entity->validate();
-
-    // Remove violations of inaccessible fields as they cannot stem from our
-    // changes.
-    $violations->filterByFieldAccess();
-
-    if (count($violations) > 0) {
-      $messages = [];
-      foreach ($violations as $violation) {
-        $messages[] = $violation->getPropertyPath() . ': ' . $violation->getMessage();
-      }
-      throw new BadRequestHttpException(implode('. ', $messages));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAllowedFormats() {
-    return $this->getPluginDefinition()['allowed_formats'];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheContexts() {
-    return [];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheTags() {
-    return $this->cacheTags;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheMaxAge() {
-    return Cache::PERMANENT;
-  }
-
-}
diff --git a/src/Plugin/ApiResource/ReplicateApiResource.php b/src/Plugin/ApiResource/ReplicateApiResource.php
deleted file mode 100644
index 77d4156..0000000
--- a/src/Plugin/ApiResource/ReplicateApiResource.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin\ApiResource;
-
-use Drupal\relaxed\Http\ApiResourceResponse;
-
-/**
- * @ApiResource(
- *   id = "replicate",
- *   label = "Replicate",
- *   serialization_class = {
- *     "canonical" = "Drupal\relaxed\Replicate\Replicate",
- *   },
- *   path = "/_replicate"
- * )
- */
-class ReplicateApiResource extends ApiResourceBase {
-
-  /**
-   * @param \Drupal\relaxed\Replicate\Replicate $replicate
-   *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
-   */
-  public function post($replicate) {
-    $replicate->doReplication();
-
-    return new ApiResourceResponse($replicate, 200);
-  }
-}
diff --git a/src/Plugin/ApiResourceInterface.php b/src/Plugin/ApiResourceInterface.php
deleted file mode 100644
index 3d296f6..0000000
--- a/src/Plugin/ApiResourceInterface.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Component\Plugin\PluginInspectionInterface;
-use Drupal\Core\Cache\CacheableDependencyInterface;
-
-/**
- * Contract for API resource plugins.
- */
-interface ApiResourceInterface extends PluginInspectionInterface, CacheableDependencyInterface {
-
-  /**
-   * An array of allowed formats this resource supports.
-   *
-   * @return array
-   */
-  public function getAllowedFormats();
-
-}
diff --git a/src/Plugin/ApiResourceManager.php b/src/Plugin/ApiResourceManager.php
deleted file mode 100644
index 9c3cfa8..0000000
--- a/src/Plugin/ApiResourceManager.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\relaxed\Annotation\ApiResource;
-
-/**
- * API resource plugin manager.
- */
-class ApiResourceManager extends DefaultPluginManager implements ApiResourceManagerInterface {
-
-  /**
-   * Constructor for ApiResourceManager objects.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler to invoke the alter hook with.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
-    parent::__construct('Plugin/ApiResource', $namespaces, $module_handler, ApiResourceInterface::class, ApiResource::class);
-
-    $this->alterInfo('relaxed_api_resource_info');
-    $this->setCacheBackend($cache_backend, 'relaxed:api_resource:plugins');
-  }
-
-}
diff --git a/src/Plugin/ApiResourceManagerInterface.php b/src/Plugin/ApiResourceManagerInterface.php
deleted file mode 100644
index 340ca55..0000000
--- a/src/Plugin/ApiResourceManagerInterface.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\relaxed\Plugin\ApiResourceManagerInterface.
- */
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Component\Plugin\PluginManagerInterface;
-
-/**
- * Contract for API resource managers.
- */
-interface ApiResourceManagerInterface extends PluginManagerInterface {
-
-}
diff --git a/src/Plugin/ApiResourceRouteGenerator.php b/src/Plugin/ApiResourceRouteGenerator.php
deleted file mode 100644
index bee8342..0000000
--- a/src/Plugin/ApiResourceRouteGenerator.php
+++ /dev/null
@@ -1,229 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Logger\LoggerChannelInterface;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * API resource route generator.
- */
-class ApiResourceRouteGenerator implements ApiResourceRouteGeneratorInterface {
-
-  /**
-   * The plugin manager for format negotiator plugins.
-   *
-   * @var \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface
-   */
-  protected $formatManager;
-
-  /**
-   * A logger instance.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelInterface
-   */
-  protected $logger;
-
-  /**
-   * Allowed request methods.
-   *
-   * CouchDB only supports these methods.
-   *
-   * @var array
-   */
-  protected $requestMethods = [
-    'HEAD',
-    'GET',
-    'POST',
-    'PUT',
-    'DELETE',
-    // This is a non-standard HTTP method implemented by CouchDB.
-    'COPY',
-  ];
-
-  /**
-   * The relaxed API root (base) path.
-   *
-   * @var string
-   */
-  protected $apiRoot;
-
-  /**
-   * The relaxed configuration.
-   *
-   * @var \Drupal\Core\Config\ImmutableConfig
-   */
-  protected $config;
-
-  /**
-   * @var array
-   */
-  protected $availableFormats;
-
-  /**
-   * The configured authentication providers for relaxed endpoints.
-   *
-   * @var array
-   */
-  protected $authenticationProviders;
-
-  /**
-   * Constructs an ApiResourceRoutes object.
-   *
-   * @param \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface $manager
-   *   The format negotiator plugin manager.
-   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
-   *   A logger instance.
-   */
-  public function __construct(FormatNegotiatorManagerInterface $format_manager, ConfigFactoryInterface $config_factory, LoggerChannelInterface $logger) {
-    $this->formatManager = $format_manager;
-    $this->logger = $logger;
-
-    $relaxed_config = $config_factory->get('relaxed.settings');
-
-    $this->apiRoot = trim($relaxed_config->get('api_root'), '/');
-    $this->authenticationProviders = $relaxed_config->get('authentication');
-  }
-
-  /**
-   * @param ApiResourceInterface $api_resource
-   * @return RouteCollection
-   */
-  public function routes(ApiResourceInterface $api_resource) {
-    $collection = new RouteCollection();
-    $definition = $api_resource->getPluginDefinition();
-    $plugin_id = $api_resource->getPluginId();
-    // Prefix all routes with 'relaxed.'.
-    $route_name = sprintf('relaxed.%s', strtr($plugin_id, ':', '.'));
-
-    foreach ($this->availableMethods($api_resource) as $method) {
-      // HEAD and GET are equivalent as per RFC and handled by the same route.
-      // @see \Symfony\Component\Routing\Matcher::matchCollection()
-      if ($method == 'HEAD') {
-        continue;
-      }
-
-      // Allow pull or push permissions depending on the method.
-      $permissions = 'perform push replication';
-
-      if ($method === 'GET') {
-        $permissions .= '+perform pull replication';
-      }
-
-      $route = new Route($this->apiRoot . $definition['path'], [
-        '_controller' => 'Drupal\relaxed\Controller\ResourceController::handle',
-        '_api_resource' => $plugin_id,
-      ], [
-        '_permission' => $permissions,
-        // @todo We might want to remove this so any format will always return the same...
-        // Add all formats we have to allowed responses on the route.
-        //'_format' => implode('|', $this->availableFormats()),
-      ],
-        [
-          'no_cache' => isset($definition['no_cache']) ? $definition['no_cache'] : FALSE,
-        ],
-        '',
-        [],
-        // The HTTP method is a requirement for this route.
-        [$method]
-      );
-
-      $route->setOption('_auth', $this->authenticationProviders());
-      //$route->addRequirements(['_content_type_format' => implode('|', $this->availableFormats($api_resource))]);
-
-      // @todo {@link https://www.drupal.org/node/2600450 Move this parameter
-      // logic to a generic route enhancer instead.}
-      $parameters = [];
-
-      foreach (['db', 'docid'] as $parameter) {
-        if (strpos($route->getPath(), '{' . $parameter . '}')) {
-          $parameters[$parameter] = ['type' => 'relaxed:' . $parameter];
-        }
-      }
-
-      if (!empty($definition['parameters'])) {
-        foreach ($definition['parameters'] as $parameter => $type) {
-          $parameters[$parameter] = ['type' => $type];
-        }
-      }
-
-      if ($parameters) {
-        $route->addOptions(['parameters' => $parameters]);
-      }
-
-      $method_lower = strtolower($method);
-      $collection->add("$route_name.$method_lower", $route);
-    }
-
-    return $collection;
-  }
-
-  /**
-   * Returns the available methods for an API resource plugin.
-   *
-   * @param ApiResourceInterface $api_resource
-   *
-   * @return array
-   */
-  protected function availableMethods(ApiResourceInterface $api_resource) {
-    $methods = $this->requestMethods();
-    $available = [];
-
-    foreach ($methods as $method) {
-      // Only expose methods where the HTTP request method exists on the plugin.
-      if (method_exists($api_resource, strtolower($method))) {
-        $available[] = $method;
-      }
-    }
-
-    return $available;
-  }
-
-  /**
-   * Returns a list of all available formats.
-   *
-   * @param \Drupal\relaxed\Plugin\ApiResourceInterface
-   *
-   * @return array
-   */
-  protected function availableFormats(ApiResourceInterface $api_resource = NULL) {
-    $resource_allowed_formats = $api_resource ? $api_resource->getAllowedFormats() : [];
-
-    if (!isset($this->availableFormats)) {
-      $this->availableFormats = $this->formatManager->availableFormats();
-    }
-
-    if (empty($resource_allowed_formats)) {
-      // Return all formats.
-      return $this->availableFormats;
-    }
-
-    // Otherwise, intersect them.
-    return array_intersect($this->availableFormats, $resource_allowed_formats);
-  }
-
-  /**
-   * Provides predefined HTTP request methods.
-   *
-   * Plugins can override this method to provide additional custom request
-   * methods.
-   *
-   * @return array
-   *   The list of allowed HTTP request method strings.
-   */
-  protected function requestMethods() {
-    return $this->requestMethods;
-  }
-
-  /**
-   * Returns the configured relaxed authentication providers.
-   *
-   * @return array
-   */
-  protected function authenticationProviders() {
-    return $this->authenticationProviders;
-  }
-
-}
diff --git a/src/Plugin/ApiResourceRouteGeneratorInterface.php b/src/Plugin/ApiResourceRouteGeneratorInterface.php
deleted file mode 100644
index 36962c4..0000000
--- a/src/Plugin/ApiResourceRouteGeneratorInterface.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin;
-
-/**
- * Contract for Api resource route generator.
- */
-interface ApiResourceRouteGeneratorInterface {
-
-  /**
-   * @param ApiResourceInterface $api_resource
-   * @return \Symfony\Component\Routing\RouteCollection
-   */
-  public function routes(ApiResourceInterface $api_resource);
-
-}
diff --git a/src/Plugin/FormatNegotiator/JsonNegotiator.php b/src/Plugin/FormatNegotiator/JsonNegotiator.php
deleted file mode 100644
index 985a61b..0000000
--- a/src/Plugin/FormatNegotiator/JsonNegotiator.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin\FormatNegotiator;
-
-
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Serializer\Serializer;
-
-/**
- * Default JSON format(s).
- *
- * This will always apply and should come last.
- *
- * @FormatNegotiator(
- *   id = "json",
- *   label = "JSON (default)",
- *   formats = {"json","mixed","related"}
- * )
- */
-class JsonNegotiator extends NegotiatorBase implements ContainerFactoryPluginInterface {
-
-  /**
-   * JsonNegotiator constructor.
-   *
-   * @param \Symfony\Component\Serializer\Serializer $serializer
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, Serializer $serializer) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-
-    $this->serializer = $serializer;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('replication.serializer')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function applies($format, $method, $type) {
-    return TRUE;
-  }
-
-}
diff --git a/src/Plugin/FormatNegotiator/NegotiatorBase.php b/src/Plugin/FormatNegotiator/NegotiatorBase.php
deleted file mode 100644
index b59e5db..0000000
--- a/src/Plugin/FormatNegotiator/NegotiatorBase.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin\FormatNegotiator;
-
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Plugin\PluginBase;
-use Drupal\relaxed\Plugin\FormatNegotiatorInterface;
-
-abstract class NegotiatorBase extends PluginBase implements FormatNegotiatorInterface {
-
-  /**
-   * @var array
-   */
-  protected $cacheTags = [];
-
-  /**
-   * @var \Symfony\Component\Serializer\Serializer
-   */
-  protected $serializer;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function serializer($format, $method, $type) {
-    return $this->serializer;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function applies($format, $method, $type) {
-    return in_array($format, $this->getPluginDefinition()['formats']);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheContexts() {
-    return [];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheTags() {
-    return $this->cacheTags;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheMaxAge() {
-    return Cache::PERMANENT;
-  }
-
-}
diff --git a/src/Plugin/FormatNegotiator/StreamNegotiator.php b/src/Plugin/FormatNegotiator/StreamNegotiator.php
deleted file mode 100644
index ece176f..0000000
--- a/src/Plugin/FormatNegotiator/StreamNegotiator.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin\FormatNegotiator;
-
-
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Serializer\Serializer;
-
-/**
- * @FormatNegotiator(
- *   id = "stream",
- *   label = "Stream / Base64 Stream",
- *   formats = {"stream","base64_stream"},
- *   priority = 10
- * )
- */
-class StreamNegotiator extends NegotiatorBase implements ContainerFactoryPluginInterface {
-
-  /**
-   * StreamNegotiator constructor.
-   *
-   * @param \Symfony\Component\Serializer\Serializer $serializer
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, Serializer $serializer) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-
-    $this->serializer = $serializer;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      // @todo Inject a different serializer for stream stuff only.
-      $container->get('replication.serializer')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function applies($format, $method, $type) {
-    // If the format doesn't match at all, we don't care anyway.
-    if (!parent::applies($format, $method, $type)) {
-      return FALSE;
-    }
-
-    // If it's applying to response data, only allow stream for GET and HEAD.
-    if ($type === 'response') {
-      return in_array($method, ['get', 'head'], TRUE);
-    }
-
-    // If this is for applying to incoming request data, it's ok.
-    // I.e. '$type === "request"'.
-    return TRUE;
-  }
-
-}
diff --git a/src/Plugin/FormatNegotiatorInterface.php b/src/Plugin/FormatNegotiatorInterface.php
deleted file mode 100644
index 235b9f8..0000000
--- a/src/Plugin/FormatNegotiatorInterface.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\relaxed\Plugin\FormatNegotiatorInterface.
- */
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Core\Cache\CacheableDependencyInterface;
-use \Symfony\Component\HttpFoundation\Request;
-
-/**
- * Contract for format negotiator plugins.
- */
-interface FormatNegotiatorInterface extends CacheableDependencyInterface {
-
-  /**
-   * Use this applies method OR the in the annotation. This was is
-   * potentially more expensive at runtime, but is way more flexible.
-   *
-   * @param string $format
-   * @param string $method
-   * @param string $type
-   *   Either 'request' or 'response'.
-   *
-   * @return bool
-   *   TRUE if this negotiator applies, FALSE otherwise.
-   */
-  public function applies($format, $method, $type);
-
-  /**
-   * Return a serializer instance.
-   *
-   * Pretty much every time would be injected into the plugin and just returned.
-   *
-   * @param string $format
-   * @param string $method
-   * @param string $type
-   *   Either 'request' or 'response'.
-   *
-   * @return \Symfony\Component\Serializer\Serializer
-   */
-  public function serializer($format, $method, $type);
-
-}
diff --git a/src/Plugin/FormatNegotiatorManager.php b/src/Plugin/FormatNegotiatorManager.php
deleted file mode 100644
index 870ee0b..0000000
--- a/src/Plugin/FormatNegotiatorManager.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\relaxed\Annotation\FormatNegotiator;
-
-/**
- * Relaxed format negotiator manager.
- */
-class FormatNegotiatorManager extends DefaultPluginManager implements FormatNegotiatorManagerInterface {
-
-  /**
-   * Constructor for FormatNegotiatorManager objects.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler to invoke the alter hook with.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
-    parent::__construct('Plugin/FormatNegotiator', $namespaces, $module_handler, FormatNegotiatorInterface::class, FormatNegotiator::class);
-
-    $this->alterInfo('relaxed_format_negotiator_info');
-    $this->setCacheBackend($cache_backend, 'relaxed:format_negotiator:plugins');
-  }
-
-  /**
-   * Returns an array of all available formats from all plugins.
-   *
-   * @return array
-   */
-  public function availableFormats() {
-    $available = [];
-    $formats = array_map(function ($definition) {
-      return $definition['formats'];
-    }, $this->getDefinitions());
-
-    foreach ($formats as $formats) {
-      $available = array_merge($available, $formats);
-    }
-
-    return array_unique($available);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function select($format, $method, $type) {
-    foreach ($this->getDefinitions() as $definition) {
-      $plugin = $this->createInstance($definition['id']);
-
-      if ($plugin->applies($format, $method, $type)) {
-        return $plugin;
-      }
-    }
-
-    return NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function findDefinitions() {
-    // Overriding findDefinitions means the definitions will be sorted before
-    // they are cached.
-    $definitions = parent::findDefinitions();
-
-    // Sort the definitions by priority.
-    uasort($definitions, function ($a, $b) {
-      $a_weight = $a['priority'];
-      $b_weight = $b['priority'];
-
-      if ($a_weight == $b_weight) {
-        return 0;
-      }
-
-      return ($a_weight < $b_weight) ? 1 : -1;
-    });
-
-    return $definitions;
-  }
-
-}
diff --git a/src/Plugin/FormatNegotiatorManagerInterface.php b/src/Plugin/FormatNegotiatorManagerInterface.php
deleted file mode 100644
index c93635b..0000000
--- a/src/Plugin/FormatNegotiatorManagerInterface.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface.
- */
-
-namespace Drupal\relaxed\Plugin;
-
-use Drupal\Component\Plugin\PluginManagerInterface;
-
-/**
- * Interface for format negotiator plugin manager.
- */
-interface FormatNegotiatorManagerInterface extends PluginManagerInterface {
-
-  /**
-   * Selects an appropriate negotiator
-   *
-   * @param string $format
-   * @param string $method
-   * @param string $type
-   *   Either 'request' or 'response'.
-   *
-   * @return \Drupal\relaxed\Plugin\FormatNegotiatorInterface|NULL
-   */
-  public function select($format, $method, $type);
-
-  /**
-   * Return a list of all available formats.
-   *
-   * @return array
-   */
-  public function availableFormats();
-
-}
diff --git a/src/Plugin/ApiResource/AllDbsApiResource.php b/src/Plugin/rest/resource/AllDbsResource.php
similarity index 79%
rename from src/Plugin/ApiResource/AllDbsApiResource.php
rename to src/Plugin/rest/resource/AllDbsResource.php
index 537090f..4981c54 100644
--- a/src/Plugin/ApiResource/AllDbsApiResource.php
+++ b/src/Plugin/rest/resource/AllDbsResource.php
@@ -1,26 +1,28 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\rest\ResourceResponse;
 use Drupal\multiversion\Entity\Workspace;
-use Drupal\relaxed\Http\ApiResourceResponse;
 
 /**
  * Implements http://docs.couchdb.org/en/latest/api/server/common.html#all-dbs
  */
 
 /**
- * @ApiResource(
- *   id = "all_dbs",
+ * @RestResource(
+ *   id = "relaxed:all_dbs",
  *   label = "All Workspaces",
  *   serialization_class = {
  *     "canonical" = "Drupal\multiversion\Entity\Workspace",
  *   },
- *   path = "/_all_dbs"
+ *   uri_paths = {
+ *     "canonical" = "/_all_dbs",
+ *   }
  * )
  */
-class AllDbsApiResource extends ApiResourceBase {
+class AllDbsResource extends ResourceBase {
 
   /**
    * Retrieve list of all entity types.
@@ -36,7 +38,7 @@ class AllDbsApiResource extends ApiResourceBase {
       $workspace_machine_names[] = $workspace->getMachineName();
     }
 
-    $response = new ApiResourceResponse($workspace_machine_names, 200);
+    $response = new ResourceResponse($workspace_machine_names, 200);
     foreach ($workspaces as $workspace) {
       $response->addCacheableDependency($workspace);
     }
diff --git a/src/Plugin/ApiResource/AllDocsApiResource.php b/src/Plugin/rest/resource/AllDocsResource.php
similarity index 81%
rename from src/Plugin/ApiResource/AllDocsApiResource.php
rename to src/Plugin/rest/resource/AllDocsResource.php
index d120424..bda73ef 100644
--- a/src/Plugin/ApiResource/AllDocsApiResource.php
+++ b/src/Plugin/rest/resource/AllDocsResource.php
@@ -1,25 +1,26 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
 use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @ApiResource(
- *   id = "all_docs",
+ * @RestResource(
+ *   id = "relaxed:all_docs",
  *   label = "All Docs",
  *   serialization_class = {
  *     "canonical" = "Drupal\relaxed\AllDocs\AllDocs",
  *   },
- *   path = "/{db}/_all_docs"
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_all_docs",
+ *   }
  * )
  */
-class AllDocsApiResource extends ApiResourceBase {
+class AllDocsResource extends ResourceBase {
 
   /**
    * @param string | \Drupal\Core\Config\Entity\ConfigEntityInterface $workspace
@@ -40,7 +41,7 @@ class AllDocsApiResource extends ApiResourceBase {
       $all_docs->includeDocs(TRUE);
     }
 
-    $response = new ApiResourceResponse($all_docs, 200);
+    $response = new ResourceResponse($all_docs, 200);
     foreach (\Drupal::service('multiversion.manager')->getSupportedEntityTypes() as $entity_type) {
       $response->addCacheableDependency($entity_type);
     }
diff --git a/src/Plugin/ApiResource/AttachmentApiResource.php b/src/Plugin/rest/resource/AttachmentResource.php
similarity index 85%
rename from src/Plugin/ApiResource/AttachmentApiResource.php
rename to src/Plugin/rest/resource/AttachmentResource.php
index d116597..35a8a94 100644
--- a/src/Plugin/ApiResource/AttachmentApiResource.php
+++ b/src/Plugin/rest/resource/AttachmentResource.php
@@ -1,34 +1,37 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\file\FileInterface;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @ApiResource(
- *   id = "attachment",
+ * @RestResource(
+ *   id = "relaxed:attachment",
  *   label = "Attachment",
  *   serialization_class = {
  *     "canonical" = "Drupal\file\Entity\File",
  *   },
- *   allowed_formats = {"stream","base64_stream"},
- *   path = "/{db}/{docid}/{field_name}/{delta}/{file_uuid}/{scheme}/{filename}",
- *   parameters = {
- *     "file_uuid" = "entity_uuid:file",
+ *   uri_paths = {
+ *     "canonical" = "/{db}/{docid}/{field_name}/{delta}/{file_uuid}/{scheme}/{filename}",
+ *   },
+ *   uri_parameters = {
+ *     "canonical" = {
+ *       "file_uuid" = "entity_uuid:file",
+ *     }
  *   }
  * )
  *
  * @todo {@link https://www.drupal.org/node/2600428 Implement real ETag.}
  */
-class AttachmentApiResource extends ApiResourceBase {
+class AttachmentResource extends ResourceBase {
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
@@ -38,7 +41,7 @@ class AttachmentApiResource extends ApiResourceBase {
    * @param string | \Drupal\file\FileInterface $file
    * @param string $scheme
    * @param string $filename
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    */
   public function head($workspace, $entity, $field_name, $delta, $file, $scheme, $filename) {
     if (!$workspace instanceof WorkspaceInterface
@@ -49,10 +52,8 @@ class AttachmentApiResource extends ApiResourceBase {
 
     if (!$entity->access('view') || !$entity->{$field_name}->access('view')) {
       throw new AccessDeniedHttpException();
-
     }
-
-    return new ApiResourceResponse(NULL, 200, $this->responseHeaders($file, ['Content-Type', 'Content-Length', 'Content-MD5', 'X-Relaxed-ETag']));
+    return new ResourceResponse(NULL, 200, $this->responseHeaders($file, ['Content-Type', 'Content-Length', 'Content-MD5', 'X-Relaxed-ETag']));
   }
 
   /**
@@ -63,7 +64,7 @@ class AttachmentApiResource extends ApiResourceBase {
    * @param string | \Drupal\file\FileInterface $file
    * @param string $scheme
    * @param string $filename
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    */
   public function get($workspace, $entity, $field_name, $delta, $file, $scheme, $filename) {
     if (!$workspace instanceof WorkspaceInterface
@@ -75,8 +76,7 @@ class AttachmentApiResource extends ApiResourceBase {
     if (!$entity->access('view') || !$entity->{$field_name}->access('view')) {
       throw new AccessDeniedHttpException();
     }
-
-    return new ApiResourceResponse($file, 200, $this->responseHeaders($file, ['Content-Type', 'Content-Length', 'Content-MD5', 'X-Relaxed-ETag']));
+    return new ResourceResponse($file, 200, $this->responseHeaders($file, ['Content-Type', 'Content-Length', 'Content-MD5', 'X-Relaxed-ETag']));
   }
 
   /**
@@ -89,7 +89,7 @@ class AttachmentApiResource extends ApiResourceBase {
    * @param string $filename
    * @param \Drupal\file\FileInterface $received_file
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    */
   public function put($workspace, $entity, $field_name, $delta, $existing_file, $scheme, $filename, FileInterface $received_file) {
     if (!$workspace instanceof WorkspaceInterface
@@ -119,7 +119,7 @@ class AttachmentApiResource extends ApiResourceBase {
       $entity->save();
 
       $data = ['ok' => TRUE, 'id' => $entity->uuid(), 'rev' => $entity->_rev->value];
-      return new ApiResourceResponse($data, 200, $this->responseHeaders($file, ['Content-MD5', 'X-Relaxed-ETag']));
+      return new ResourceResponse($data, 200, $this->responseHeaders($file, ['Content-MD5', 'X-Relaxed-ETag']));
     }
     // @todo {@link https://www.drupal.org/node/2599912 Catch more generic
     // exceptions here and on other places.}
@@ -137,7 +137,7 @@ class AttachmentApiResource extends ApiResourceBase {
    * @param string $scheme
    * @param string $filename
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function delete($workspace, $entity, $field_name, $delta, $file, $scheme, $filename) {
     if (!$workspace instanceof WorkspaceInterface
@@ -161,7 +161,7 @@ class AttachmentApiResource extends ApiResourceBase {
       $entity->save();
       $rev = $entity->_rev->value;
       $data = ['ok' => TRUE, 'id' => $entity->uuid(), 'rev' => $rev];
-      return new ApiResourceResponse($data, 200, $this->responseHeaders($file, ['X-Relaxed-ETag']), $rev);
+      return new ResourceResponse($data, 200, ['X-Relaxed-ETag'], $rev);
     }
     catch (\Exception $e) {
       throw new HttpException(500, $e->getMessage());
diff --git a/src/Plugin/ApiResource/BulkDocsApiResource.php b/src/Plugin/rest/resource/BulkDocsResource.php
similarity index 64%
rename from src/Plugin/ApiResource/BulkDocsApiResource.php
rename to src/Plugin/rest/resource/BulkDocsResource.php
index ca8bb28..377577a 100644
--- a/src/Plugin/ApiResource/BulkDocsApiResource.php
+++ b/src/Plugin/rest/resource/BulkDocsResource.php
@@ -1,27 +1,29 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @ApiResource(
- *   id = "bulk_docs",
+ * @RestResource(
+ *   id = "relaxed:bulk_docs",
  *   label = "Bulk documents",
  *   serialization_class = {
  *     "canonical" = "Drupal\replication\BulkDocs\BulkDocs",
  *   },
- *   path = "/{db}/_bulk_docs"
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_bulk_docs",
+ *   }
  * )
  */
-class BulkDocsApiResource extends ApiResourceBase {
+class BulkDocsResource extends ResourceBase {
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param \Drupal\replication\BulkDocs\BulkDocsInterface $bulk_docs
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function post($workspace, $bulk_docs) {
     if (!$workspace instanceof WorkspaceInterface) {
@@ -29,6 +31,6 @@ class BulkDocsApiResource extends ApiResourceBase {
     }
 
     $bulk_docs->save();
-    return new ApiResourceResponse($bulk_docs, 201);
+    return new ResourceResponse($bulk_docs, 201);
   }
 }
diff --git a/src/Plugin/ApiResource/ChangesApiResource.php b/src/Plugin/rest/resource/ChangesResource.php
similarity index 75%
rename from src/Plugin/ApiResource/ChangesApiResource.php
rename to src/Plugin/rest/resource/ChangesResource.php
index 20d6a49..13bf87b 100644
--- a/src/Plugin/ApiResource/ChangesApiResource.php
+++ b/src/Plugin/rest/resource/ChangesResource.php
@@ -1,37 +1,36 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
 use Drupal\replication\Changes\ChangesInterface;
 use Drupal\replication\ChangesFactoryInterface;
+use Drupal\rest\ResourceResponse;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @ApiResource(
- *   id = "changes",
+ * @RestResource(
+ *   id = "relaxed:changes",
  *   label = "Changes",
  *   serialization_class = {
  *     "canonical" = "Drupal\replication\Changes\Changes",
  *   },
- *   path = "/{db}/_changes",
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_changes",
+ *   },
  *   no_cache = TRUE
  * )
  */
-class ChangesApiResource extends ApiResourceBase implements ContainerFactoryPluginInterface {
+class ChangesResource extends ResourceBase {
 
-  /**
-   * @var \Drupal\replication\ChangesFactoryInterface
-   */
+  /** @var ChangesFactoryInterface  */
   protected $changesFactory;
 
   /**
-   * Constructs a Drupal\rest\Plugin\ApiResourceBase object.
+   * Constructs a Drupal\rest\Plugin\ResourceBase object.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.
@@ -46,9 +45,8 @@ class ChangesApiResource extends ApiResourceBase implements ContainerFactoryPlug
    * @param \Drupal\replication\ChangesFactoryInterface $changes_factory
    *  The ChangesFactory service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, ChangesFactoryInterface $changes_factory) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, ChangesFactoryInterface $changes_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
     $this->changesFactory = $changes_factory;
   }
 
@@ -60,6 +58,8 @@ class ChangesApiResource extends ApiResourceBase implements ContainerFactoryPlug
       $configuration,
       $plugin_id,
       $plugin_definition,
+      $container->getParameter('serializer.formats'),
+      $container->get('logger.factory')->get('rest'),
       $container->get('replication.changes_factory')
     );
   }
@@ -77,7 +77,7 @@ class ChangesApiResource extends ApiResourceBase implements ContainerFactoryPlug
       $changes->includeDocs(TRUE);
     }
 
-    return new ApiResourceResponse($changes, 200);
+    return new ResourceResponse($changes, 200);
   }
 
 }
diff --git a/src/Plugin/ApiResource/DbApiResource.php b/src/Plugin/rest/resource/DbResource.php
similarity index 86%
rename from src/Plugin/ApiResource/DbApiResource.php
rename to src/Plugin/rest/resource/DbResource.php
index 8fafa09..613ae96 100644
--- a/src/Plugin/ApiResource/DbApiResource.php
+++ b/src/Plugin/rest/resource/DbResource.php
@@ -1,11 +1,12 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
+use Drupal\multiversion\Entity\Workspace;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Drupal\user\UserInterface;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -14,30 +15,32 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
 
 /**
- * @ApiResource(
- *   id = "db",
+ * @RestResource(
+ *   id = "relaxed:db",
  *   label = "Workspace",
  *   serialization_class = {
  *     "canonical" = "Drupal\multiversion\Entity\WorkspaceInterface",
  *     "post" = "Drupal\Core\Entity\ContentEntityInterface",
  *   },
- *   path = "/{db}",
+ *   uri_paths = {
+ *     "canonical" = "/{db}",
+ *   },
  *   no_cache = TRUE
  * )
  */
-class DbApiResource extends ApiResourceBase {
+class DbResource extends ResourceBase {
 
   /**
    * @param $entity
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
    */
   public function head($entity) {
     if (!$entity instanceof WorkspaceInterface) {
       throw new NotFoundHttpException();
     }
-    $response = new ApiResourceResponse(NULL, 200);
+    $response = new ResourceResponse(NULL, 200);
     $response->addCacheableDependency($entity);
 
     return $response;
@@ -46,7 +49,7 @@ class DbApiResource extends ApiResourceBase {
   /**
    * @param $entity
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
    */
   public function get($entity) {
@@ -54,7 +57,7 @@ class DbApiResource extends ApiResourceBase {
       throw new NotFoundHttpException();
     }
     // @todo: {@link https://www.drupal.org/node/2600382 Access check.}
-    $response =  new ApiResourceResponse($entity, 200);
+    $response =  new ResourceResponse($entity, 200);
     $response->addCacheableDependency($entity);
 
     return $response;
@@ -63,7 +66,7 @@ class DbApiResource extends ApiResourceBase {
   /**
    * @param $entity
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    * @throws \Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException
@@ -85,7 +88,7 @@ class DbApiResource extends ApiResourceBase {
     catch (EntityStorageException $e) {
       throw new HttpException(500, t('Internal server error'), $e);
     }
-    $response = new ApiResourceResponse(['ok' => TRUE], 201);
+    $response = new ResourceResponse(['ok' => TRUE], 201);
     $response->addCacheableDependency($entity);
 
     return $response;
@@ -95,7 +98,7 @@ class DbApiResource extends ApiResourceBase {
    * @param $workspace
    * @param ContentEntityInterface $entity
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    * @throws \Symfony\Component\HttpKernel\Exception\ConflictHttpException
    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
@@ -106,7 +109,7 @@ class DbApiResource extends ApiResourceBase {
     // If the workspace parameter is a string it means it could not be upcasted
     // to an entity because none existed.
     if (!$workspace instanceof WorkspaceInterface) {
-      throw new NotFoundHttpException(t('Database does not exist'));
+      throw new NotFoundHttpException(t('Database does not exist')); 
     }
     elseif (empty($entity)) {
       throw new BadRequestHttpException(t('No content received'));
@@ -133,7 +136,7 @@ class DbApiResource extends ApiResourceBase {
     try {
       $entity->save();
       $rev = $entity->_rev->value;
-      $response = new ApiResourceResponse(['ok' => TRUE, 'id' => $entity->uuid(), 'rev' => $rev], 201, ['ETag' => $rev]);
+      $response = new ResourceResponse(['ok' => TRUE, 'id' => $entity->uuid(), 'rev' => $rev], 201, ['ETag' => $rev]);
       $response->addCacheableDependency($entity);
 
       return $response;
@@ -146,7 +149,7 @@ class DbApiResource extends ApiResourceBase {
   /**
    * @param WorkspaceInterface $entity
    *
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
   public function delete(WorkspaceInterface $entity) {
@@ -157,7 +160,7 @@ class DbApiResource extends ApiResourceBase {
     catch (\Exception $e) {
       throw new HttpException(500, NULL, $e);
     }
-    $response = new ApiResourceResponse(['ok' => TRUE], 200);
+    $response = new ResourceResponse(['ok' => TRUE], 200);
     $response->addCacheableDependency($entity);
 
     return $response;
diff --git a/src/Plugin/ApiResource/DocApiResource.php b/src/Plugin/rest/resource/DocResource.php
similarity index 88%
rename from src/Plugin/ApiResource/DocApiResource.php
rename to src/Plugin/rest/resource/DocResource.php
index 7e65537..ccdbad1 100644
--- a/src/Plugin/ApiResource/DocApiResource.php
+++ b/src/Plugin/rest/resource/DocResource.php
@@ -1,14 +1,14 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\file\FileInterface;
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
 use Drupal\relaxed\HttpMultipart\ResourceMultipartResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
@@ -18,25 +18,27 @@ use Drupal\relaxed\HttpMultipart\Message\MultipartResponse as MultipartResponseP
 use GuzzleHttp\Psr7;
 
 /**
- * @ApiResource(
- *   id = "doc",
+ * @RestResource(
+ *   id = "relaxed:doc",
  *   label = "Document",
  *   serialization_class = {
  *     "canonical" = "Drupal\Core\Entity\ContentEntityInterface",
  *   },
- *   path = "/{db}/{docid}",
+ *   uri_paths = {
+ *     "canonical" = "/{db}/{docid}",
+ *   },
  *   no_cache = TRUE
  * )
  *
  * @todo {@link https://www.drupal.org/node/2600428 Implement real ETag.}
  */
-class DocApiResource extends ApiResourceBase {
+class DocResource extends ResourceBase {
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param mixed $existing
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function head($workspace, $existing) {
     if (!$workspace instanceof WorkspaceInterface || is_string($existing)) {
@@ -52,14 +54,14 @@ class DocApiResource extends ApiResourceBase {
     }
 
     // @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber
-    return new ApiResourceResponse(NULL, 200, ['X-Relaxed-ETag' => $revisions[0]->_rev->value]);
+    return new ResourceResponse(NULL, 200, ['X-Relaxed-ETag' => $revisions[0]->_rev->value]);
   }
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param mixed $existing
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function get($workspace, $existing) {
     if (!$workspace instanceof WorkspaceInterface || is_string($existing)) {
@@ -91,9 +93,8 @@ class DocApiResource extends ApiResourceBase {
       if ($request->headers->get('Accept') === 'multipart/mixed'
         || ($request->headers->get('Accept') === '*/*' && $request->headers->get('multipart') === 'mixed')) {
         foreach ($revisions as $revision) {
-          $parts[] = new ApiResourceResponse($revision, 200, ['Content-Type' => 'application/json']);
+          $parts[] = new ResourceResponse($revision, 200, ['Content-Type' => 'application/json']);
         }
-
         return new ResourceMultipartResponse($parts, 200, ['Content-Type' => 'multipart/mixed']);
       }
       else {
@@ -110,7 +111,7 @@ class DocApiResource extends ApiResourceBase {
       $result = $revisions[0];
     }
 
-    return new ApiResourceResponse($result, 200, ['X-Relaxed-ETag' => $revisions[0]->_rev->value]);
+    return new ResourceResponse($result, 200, ['X-Relaxed-ETag' => $revisions[0]->_rev->value]);
   }
 
   /**
@@ -119,7 +120,7 @@ class DocApiResource extends ApiResourceBase {
    * @param \Drupal\Core\Entity\ContentEntityInterface $received_entity
    * @param \Symfony\Component\HttpFoundation\Request $request
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function put($workspace, $existing_entity, ContentEntityInterface $received_entity, Request $request) {
     if (!$workspace instanceof WorkspaceInterface) {
@@ -156,7 +157,7 @@ class DocApiResource extends ApiResourceBase {
       $received_entity->save();
       $rev = $received_entity->_rev->value;
       $data = ['ok' => TRUE, 'id' => $received_entity->uuid(), 'rev' => $rev];
-      return new ApiResourceResponse($data, 201, ['X-Relaxed-ETag' => $rev]);
+      return new ResourceResponse($data, 201, ['X-Relaxed-ETag' => $rev]);
     }
     catch (EntityStorageException $e) {
       throw new HttpException(500, $e->getMessage());
@@ -167,7 +168,7 @@ class DocApiResource extends ApiResourceBase {
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param string | \Drupal\Core\Entity\ContentEntityInterface $entity
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function delete($workspace, $entity) {
     if (!($workspace instanceof WorkspaceInterface)
@@ -192,7 +193,7 @@ class DocApiResource extends ApiResourceBase {
       throw new HttpException(500, NULL, $e);
     }
 
-    return new ApiResourceResponse(['ok' => TRUE], 200);
+    return new ResourceResponse(['ok' => TRUE], 200);
   }
 
 }
diff --git a/src/Plugin/ApiResource/EnsureFullCommitApiResource.php b/src/Plugin/rest/resource/EnsureFullCommitResource.php
similarity index 70%
rename from src/Plugin/ApiResource/EnsureFullCommitApiResource.php
rename to src/Plugin/rest/resource/EnsureFullCommitResource.php
index e031418..338e148 100644
--- a/src/Plugin/ApiResource/EnsureFullCommitApiResource.php
+++ b/src/Plugin/rest/resource/EnsureFullCommitResource.php
@@ -1,9 +1,9 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
@@ -11,16 +11,18 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  * of transactions across multiple requests. This resource only exists to comply
  * with the replication protocol.
  *
- * @ApiResource(
- *   id = "ensure_full_commit",
+ * @RestResource(
+ *   id = "relaxed:ensure_full_commit",
  *   label = "Ensure Full Commit",
  *   serialization_class = {
  *     "canonical" = "Drupal\multiversion\Entity\WorkspaceInterface",
  *   },
- *   path = "/{db}/_ensure_full_commit",
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_ensure_full_commit",
+ *   }
  * )
  */
-class EnsureFullCommitApiResource extends ApiResourceBase {
+class EnsureFullCommitResource extends ResourceBase {
 
   public function post($workspace) {
     if (!$workspace instanceof WorkspaceInterface) {
@@ -32,6 +34,6 @@ class EnsureFullCommitApiResource extends ApiResourceBase {
       'instance_start_time' => (string) $workspace->getStartTime(),
     ];
 
-    return new ApiResourceResponse($response_data, 201);
+    return new ResourceResponse($response_data, 201);
   }
 }
diff --git a/src/Plugin/ApiResource/LocalDocResource.php b/src/Plugin/rest/resource/LocalDocResource.php
similarity index 87%
rename from src/Plugin/ApiResource/LocalDocResource.php
rename to src/Plugin/rest/resource/LocalDocResource.php
index e5d48f5..079998b 100644
--- a/src/Plugin/ApiResource/LocalDocResource.php
+++ b/src/Plugin/rest/resource/LocalDocResource.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\multiversion\Entity\WorkspaceInterface;
@@ -9,22 +9,24 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @ApiResource(
- *   id = "local:doc",
+ * @RestResource(
+ *   id = "relaxed:local:doc",
  *   label = "Local document",
  *   serialization_class = {
  *     "canonical" = "Drupal\Core\Entity\ContentEntityInterface",
  *   },
- *   path = "/{db}/_local/{docid}"
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_local/{docid}",
+ *   }
  * )
  */
-class LocalDocResource extends DocApiResource {
+class LocalDocResource extends DocResource {
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param mixed $existing
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function head($workspace, $existing) {
     if (!$workspace instanceof WorkspaceInterface || is_string($existing)) {
@@ -44,7 +46,7 @@ class LocalDocResource extends DocApiResource {
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param mixed $existing
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function get($workspace, $existing) {
     if (!$workspace instanceof WorkspaceInterface || is_string($existing)) {
@@ -66,11 +68,11 @@ class LocalDocResource extends DocApiResource {
    * @param \Drupal\Core\Entity\ContentEntityInterface $received_entity
    * @param \Symfony\Component\HttpFoundation\Request $request
    *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function put($workspace, $existing_entity, ContentEntityInterface $received_entity, Request $request) {
     if (!$received_entity->getEntityType()->get('local')) {
-      throw new BadRequestHttpException('This endpoint only supports local entity types.');
+      throw new BadRequestHttpException('This endpoint only support local entity types.');
     }
     return parent::put($workspace, $existing_entity, $received_entity, $request);
   }
diff --git a/src/Plugin/rest/resource/RelaxedResourceInterface.php b/src/Plugin/rest/resource/RelaxedResourceInterface.php
new file mode 100644
index 0000000..39a846a
--- /dev/null
+++ b/src/Plugin/rest/resource/RelaxedResourceInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\relaxed\Plugin\rest\resource;
+
+use Drupal\rest\Plugin\ResourceInterface;
+
+interface RelaxedResourceInterface extends ResourceInterface {
+
+  /**
+   * Returns whether or not this is an attachment resource.
+   * @return boolean
+   */
+  public function isAttachment();
+
+}
diff --git a/src/Plugin/rest/resource/ReplicateResource.php b/src/Plugin/rest/resource/ReplicateResource.php
new file mode 100644
index 0000000..eed8b85
--- /dev/null
+++ b/src/Plugin/rest/resource/ReplicateResource.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\relaxed\Plugin\rest\resource;
+
+use Drupal\rest\ResourceResponse;
+
+/**
+ * @RestResource(
+ *   id = "relaxed:replicate",
+ *   label = "Replicate",
+ *   serialization_class = {
+ *     "canonical" = "Drupal\relaxed\Replicate\Replicate",
+ *   },
+ *   uri_paths = {
+ *     "canonical" = "/_replicate",
+ *   }
+ * )
+ */
+class ReplicateResource extends ResourceBase {
+
+  /**
+   * @param \Drupal\relaxed\Replicate\Replicate $replicate
+   *
+   * @return \Drupal\rest\ResourceResponse
+   */
+  public function post($replicate) {
+    $replicate->doReplication();
+
+    return new ResourceResponse($replicate, 200);
+  }
+}
diff --git a/src/Plugin/rest/resource/ResourceBase.php b/src/Plugin/rest/resource/ResourceBase.php
new file mode 100644
index 0000000..b2dbc9d
--- /dev/null
+++ b/src/Plugin/rest/resource/ResourceBase.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\relaxed\Plugin\rest\resource;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\rest\Plugin\ResourceBase as CoreResourceBase;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+abstract class ResourceBase extends CoreResourceBase implements RelaxedResourceInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function routes() {
+    $this->serializerFormats = array_merge($this->serializerFormats, ['mixed', 'related']);
+    $collection = new RouteCollection();
+    $definition = $this->getPluginDefinition();
+    $api_root = trim(\Drupal::config('relaxed.settings')->get('api_root'), '/');
+    $route_name = strtr($this->pluginId, ':', '.');
+
+    foreach ($this->availableMethods() as $method) {
+      // HEAD and GET are equivalent as per RFC and handled by the same route.
+      // @see \Symfony\Component\Routing\Matcher::matchCollection()
+      if ($method == 'HEAD') {
+        continue;
+      }
+
+      // Allow pull or push permissions depending on the method.
+      $permissions = 'perform push replication';
+      if ($method === 'GET') {
+        $permissions .= '+perform pull replication';
+      }
+
+      $method_lower = strtolower($method);
+      $route = new Route($api_root . $definition['uri_paths']['canonical'], [
+        '_controller' => 'Drupal\relaxed\Controller\ResourceController::handle',
+        '_plugin' => $this->pluginId,
+      ], [
+        '_permission' => "restful " . $method_lower . " $this->pluginId" . "+$permissions",
+      ],
+        [
+          'no_cache' => isset($definition['no_cache']) ? $definition['no_cache'] : FALSE,
+        ],
+        '',
+        [],
+        // The HTTP method is a requirement for this route.
+        [$method]
+      );
+
+      if (isset($definition['uri_paths'][$method_lower])) {
+        $route->setPath($definition['uri_paths'][$method_lower]);
+      }
+
+      // @todo {@link https://www.drupal.org/node/2600450 Move this parameter
+      // logic to a generic route enhancer instead.}
+      $parameters = [];
+      foreach (['db', 'docid'] as $parameter) {
+        if (strpos($route->getPath(), '{' . $parameter . '}')) {
+          $parameters[$parameter] = ['type' => 'relaxed:' . $parameter];
+        }
+      }
+      if (!empty($definition['uri_parameters']['canonical'])) {
+        foreach ($definition['uri_parameters']['canonical'] as $parameter => $type) {
+          $parameters[$parameter] = ['type' => $type];
+        }
+      }
+      if ($parameters) {
+        $route->addOptions(['parameters' => $parameters]);
+      }
+
+      switch ($method) {
+        case 'POST':
+        case 'PUT':
+          // Restrict on the Content-Type header.
+          if (!$this->isAttachment()) {
+            $route->addRequirements(['_content_type_format' => implode('|', $this->serializerFormats)]);
+          }
+          $collection->add("$route_name.$method", $route);
+          break;
+
+        case 'GET':
+          $collection->add("$route_name.$method", $route);
+          break;
+
+        case 'DELETE':
+          foreach ($this->serializerFormats as $format) {
+            $format_route = clone $route;
+            $format_route->addRequirements(['_format' => $format]);
+            $collection->add("$route_name.$method.$format", $format_route);
+          }
+          break;
+      }
+    }
+    return $collection;
+  }
+
+  public function isAttachment() {
+    return (substr($this->getPluginId(), -strlen('attachment')) == 'attachment');
+  }
+
+  protected function validate(ContentEntityInterface $entity) {
+    $violations = $entity->validate();
+
+    // Remove violations of inaccessible fields as they cannot stem from our
+    // changes.
+    $violations->filterByFieldAccess();
+
+    if (count($violations) > 0) {
+      $messages = [];
+      foreach ($violations as $violation) {
+        $messages[] = $violation->getPropertyPath() . ': ' . $violation->getMessage();
+      }
+      throw new BadRequestHttpException(implode('. ', $messages));
+    }
+  }
+}
diff --git a/src/Plugin/ApiResource/RevsDiffApiResource.php b/src/Plugin/rest/resource/RevsDiffResource.php
similarity index 69%
rename from src/Plugin/ApiResource/RevsDiffApiResource.php
rename to src/Plugin/rest/resource/RevsDiffResource.php
index b3ac9bb..7ce9d87 100644
--- a/src/Plugin/ApiResource/RevsDiffApiResource.php
+++ b/src/Plugin/rest/resource/RevsDiffResource.php
@@ -1,29 +1,30 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
 use Drupal\multiversion\Entity\WorkspaceInterface;
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
- * @ApiResource(
- *   id = "revs_diff",
+ * @RestResource(
+ *   id = "relaxed:revs_diff",
  *   label = "Revisions diff",
  *   serialization_class = {
  *     "canonical" = "Drupal\replication\RevisionDiff\RevisionDiff",
  *   },
- *   path = "/{db}/_revs_diff",
+ *   uri_paths = {
+ *     "canonical" = "/{db}/_revs_diff",
+ *   },
  *   no_cache = TRUE
  * )
  */
-class RevsDiffApiResource extends ApiResourceBase {
+class RevsDiffResource extends ResourceBase {
 
   /**
    * @param string | \Drupal\multiversion\Entity\WorkspaceInterface $workspace
    * @param \Drupal\replication\RevisionDiff\RevisionDiffInterface $revs_diff
-   *
-   * @return \Drupal\relaxed\Http\ApiResourceResponse
+   * @return \Drupal\rest\ResourceResponse
    */
   public function post($workspace, $revs_diff) {
     if (!$workspace instanceof WorkspaceInterface) {
@@ -33,7 +34,7 @@ class RevsDiffApiResource extends ApiResourceBase {
       throw new BadRequestHttpException(t('No content info received'));
     }
 
-    return new ApiResourceResponse($revs_diff, 200);
+    return new ResourceResponse($revs_diff, 200);
   }
 
 }
diff --git a/src/Plugin/ApiResource/RootApiResource.php b/src/Plugin/rest/resource/RootResource.php
similarity index 56%
rename from src/Plugin/ApiResource/RootApiResource.php
rename to src/Plugin/rest/resource/RootResource.php
index 05ab53b..01f3f61 100644
--- a/src/Plugin/ApiResource/RootApiResource.php
+++ b/src/Plugin/rest/resource/RootResource.php
@@ -1,23 +1,25 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 
 /**
- * @ApiResource(
- *   id = "root",
+ * @RestResource(
+ *   id = "relaxed:root",
  *   label = "Root",
- *   path = ""
+ *   uri_paths = {
+ *     "canonical" = "",
+ *   }
  * )
  */
-class RootApiResource extends ApiResourceBase {
+class RootResource extends ResourceBase {
 
   /**
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    */
   public function get() {
-    return new ApiResourceResponse(
+    return new ResourceResponse(
       [
         'couchdb' => t('Welcome'),
         'uuid' => \Drupal::config('system.site')->get('uuid'),
diff --git a/src/Plugin/ApiResource/SessionApiResource.php b/src/Plugin/rest/resource/SessionResource.php
similarity index 53%
rename from src/Plugin/ApiResource/SessionApiResource.php
rename to src/Plugin/rest/resource/SessionResource.php
index 106a634..2e5b82f 100644
--- a/src/Plugin/ApiResource/SessionApiResource.php
+++ b/src/Plugin/rest/resource/SessionResource.php
@@ -1,22 +1,24 @@
 <?php
 
-namespace Drupal\relaxed\Plugin\ApiResource;
+namespace Drupal\relaxed\Plugin\rest\resource;
 
-use Drupal\relaxed\Http\ApiResourceResponse;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 use Drupal\Core\Cache\CacheableMetadata;
 
 /**
- * @ApiResource(
+ * @RestResource(
  *   id = "relaxed:session",
  *   label = "Session",
- *   path = "/_session"
+ *   uri_paths = {
+ *     "canonical" = "/_session",
+ *   }
  * )
  */
-class SessionApiResource extends ApiResourceBase {
+class SessionResource extends ResourceBase {
 
   /**
-   * @return ApiResourceResponse
+   * @return ResourceResponse
    */
   public function get() {
     $account = \Drupal::currentUser();
@@ -24,22 +26,13 @@ class SessionApiResource extends ApiResourceBase {
       throw new UnauthorizedHttpException('', 'Username or password was not recognized.');
     }
 
-    $role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
-    $roles = $account->getRoles();
-
-    // Query the users roles to see if any have admin.
-    $admin_role_count = $role_storage->getQuery()
-      ->condition('id', $roles)
-      ->condition('is_admin', TRUE)
-      ->count()
-      ->execute();
-
-    // Add computed '_admin' role to list if user has any admin flagged role.
-    if ($admin_role_count > 0) {
+    $roles = array_values($account->getRoles());
+    $admin_role = \Drupal::config('user.settings')->get('admin_role');
+    if (in_array($admin_role, $roles)) {
       $roles[] = '_admin';
     }
 
-    $response = new ApiResourceResponse(
+    $response = new ResourceResponse(
       [
         'info' => [],
         'ok' => TRUE,
diff --git a/src/Routing/ApiResourceRoutes.php b/src/Routing/ApiResourceRoutes.php
deleted file mode 100644
index 62f0737..0000000
--- a/src/Routing/ApiResourceRoutes.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-namespace Drupal\relaxed\Routing;
-
-use Drupal\Core\Logger\LoggerChannelInterface;
-use Drupal\Core\Routing\RouteSubscriberBase;
-use Drupal\relaxed\Plugin\ApiResourceManagerInterface;
-use Drupal\relaxed\Plugin\ApiResourceRouteGeneratorInterface;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Subscriber for REST-style routes.
- */
-class ApiResourceRoutes extends RouteSubscriberBase {
-
-  /**
-   * The plugin manager for API resource plugins.
-   *
-   * @var \Drupal\relaxed\Plugin\ApiResourceManagerInterface
-   */
-  protected $manager;
-
-  /**
-   * The API resource route generator.
-   *
-   * @var \Drupal\relaxed\Plugin\ApiResourceRouteGeneratorInterface
-   */
-  protected $generator;
-
-  /**
-   * A logger instance.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelInterface
-   */
-  protected $logger;
-
-  /**
-   * Constructs an ApiResourceRoutes object.
-   *
-   * @param \Drupal\relaxed\Plugin\ApiResourceManagerInterface $manager
-   *   The resource plugin manager.
-   * @param \Drupal\relaxed\Plugin\ApiResourceRouteGeneratorInterface $generator
-   *   The resource route generator.
-   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
-   *   A logger instance.
-   */
-  public function __construct(ApiResourceManagerInterface $manager, ApiResourceRouteGeneratorInterface $generator, LoggerChannelInterface $logger) {
-    $this->manager = $manager;
-    $this->generator = $generator;
-    $this->logger = $logger;
-  }
-
-  /**
-   * Alters existing routes for a specific collection.
-   *
-   * @param \Symfony\Component\Routing\RouteCollection $collection
-   *   The route collection for adding routes.
-   * @return array
-   */
-  protected function alterRoutes(RouteCollection $collection) {
-    // Iterate over all API resource plugins.
-    foreach ($this->manager->getDefinitions() as $definition) {
-      /** @var \Drupal\relaxed\Plugin\ApiResourceInterface $api_resource */
-      $api_resource = $this->manager->createInstance($definition['id'], $definition);
-
-      // Use the generator.
-      $api_resource_routes = $this->generator->routes($api_resource);
-      $collection->addCollection($api_resource_routes);
-    }
-  }
-
-}
diff --git a/src/SensitiveDataTransformer.php b/src/SensitiveDataTransformer.php
new file mode 100644
index 0000000..58d14a5
--- /dev/null
+++ b/src/SensitiveDataTransformer.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\relaxed;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\encrypt\Entity\EncryptionProfile;
+
+/**
+ * Class SensitiveDataTransformer.
+ *
+ * @package Drupal\relaxed
+ */
+class SensitiveDataTransformer {
+
+  /**
+   * Encryption is enabled or not.
+   *
+   * @var bool
+   */
+  protected $encrypt;
+
+  /**
+   * The encryption profile to use.
+   *
+   * @var string
+   */
+  protected $encryptProfile;
+
+  /**
+   * Encryption service.
+   *
+   * @var Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryption;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Constructs an Encrypt object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A config factory for retrieving required config objects.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $settings = $config_factory->get('relaxed.settings');
+    $this->encrypt = $settings->get('encrypt');
+    $this->encryptProfile = $settings->get('encrypt_profile');
+    if ($this->encrypt && $this->encryptProfile) {
+      $this->setEncryption();
+    }
+  }
+
+  /**
+   * Sets up encryption if needed.
+   */
+  public function setEncryption() {
+    // Load the encryption profile if available.
+    $this->encryption = \Drupal::service('encryption');
+    $this->encryptionProfile = EncryptionProfile::load($this->encryptProfile);
+  }
+
+  /**
+   * Gets a value.
+   *
+   * @param string $value
+   *   The value to transform.
+   *
+   * @return string
+   *   The transformed value depending on the settings.
+   */
+  public function get($value) {
+    $value = base64_decode($value);
+
+    if ($this->encrypt) {
+      $value = $this->encryption->decrypt($value, $this->encryptionProfile);
+    }
+
+    return $value;
+  }
+
+  /**
+   * Sets a value.
+   *
+   * @param string $value
+   *   The value to transform.
+   *
+   * @return string
+   *   The transformed value depending on the settings.
+   */
+  public function set($value) {
+    if ($this->encrypt) {
+      $value = $this->encryption->encrypt($value, $this->encryptionProfile);
+    }
+
+    return base64_encode($value);
+  }
+
+}
diff --git a/src/Tests/AllDbsResourceTest.php b/src/Tests/AllDbsResourceTest.php
index f172247..e005d75 100644
--- a/src/Tests/AllDbsResourceTest.php
+++ b/src/Tests/AllDbsResourceTest.php
@@ -13,8 +13,11 @@ use Drupal\multiversion\Entity\Workspace;
 class AllDbsResourceTest extends ResourceTestBase {
 
   public function testGet() {
+    $this->enableService('relaxed:all_dbs', 'GET');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'perform pull replication';
+    $permissions = $this->entityPermissions('workspace', 'view');
+    $permissions[] = 'restful get relaxed:all_dbs';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
@@ -22,7 +25,6 @@ class AllDbsResourceTest extends ResourceTestBase {
     foreach (Workspace::loadMultiple() as $workspace) {
       $workspaces[] = $workspace->getMachineName();
     }
-
     for ($i = 0; $i < 3; $i++) {
       $machine_name = $this->randomMachineName();
       $entity = Workspace::create(['machine_name' => $machine_name, 'type' => 'basic']);
diff --git a/src/Tests/AllDocsResourceTest.php b/src/Tests/AllDocsResourceTest.php
index 09dfc5a..c02f04b 100644
--- a/src/Tests/AllDocsResourceTest.php
+++ b/src/Tests/AllDocsResourceTest.php
@@ -12,11 +12,13 @@ use Drupal\Component\Serialization\Json;
 class AllDocsResourceTest extends ResourceTestBase {
 
   public function testGet() {
-    $serializer = \Drupal::service('replication.serializer');
+    $this->enableService('relaxed:all_docs', 'GET');
+    $serializer = \Drupal::service('serializer');
 
     // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('workspace', 'view');
     $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform pull replication';
+    $permissions[] = 'restful get relaxed:all_docs';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
diff --git a/src/Tests/AttachmentResourceTest.php b/src/Tests/AttachmentResourceTest.php
index bc460ba..b8d983e 100644
--- a/src/Tests/AttachmentResourceTest.php
+++ b/src/Tests/AttachmentResourceTest.php
@@ -16,6 +16,7 @@ use Drupal\file\Entity\File;
 class AttachmentResourceTest extends ResourceTestBase {
 
   public static $modules = [
+    'rest',
     'entity_test',
     'file',
     'image'
@@ -35,9 +36,13 @@ class AttachmentResourceTest extends ResourceTestBase {
     parent::setUp();
 
     // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('entity_test_rev', 'view');
+    $permissions = array_merge($permissions, $this->entityPermissions('entity_test_rev', 'create'));
+    $permissions = array_merge($permissions, $this->entityPermissions('entity_test_rev', 'delete'));
     $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform pull replication';
-    $permissions[] = 'perform push replication';
+    $permissions[] = 'restful get relaxed:attachment';
+    $permissions[] = 'restful put relaxed:attachment';
+    $permissions[] = 'restful delete relaxed:attachment';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
@@ -122,6 +127,8 @@ class AttachmentResourceTest extends ResourceTestBase {
   }
 
   public function testHead() {
+    $this->enableService('relaxed:attachment', 'GET');
+
     $file_contents = file_get_contents($this->files['1']->getFileUri());
     $encoded_digest = base64_encode(md5($file_contents));
 
@@ -137,7 +144,7 @@ class AttachmentResourceTest extends ResourceTestBase {
     $encoded_digest = base64_encode(md5($file_contents));
 
     $attachment_info = 'field_test_file/1/' . $this->files['2']->uuid() . '/public/' . $this->files['2']->getFileName();
-    $response = $this->httpRequest("$this->dbname/" . $this->entity->uuid() . "/$attachment_info", 'HEAD', NULL);
+    $this->httpRequest("$this->dbname/" . $this->entity->uuid() . "/$attachment_info", 'HEAD', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
     $this->assertHeader('content-type', 'text/plain; charset=UTF-8');
     $this->assertHeader('content-length', $this->files['2']->getSize());
@@ -157,6 +164,8 @@ class AttachmentResourceTest extends ResourceTestBase {
   }
 
   public function testGet() {
+    $this->enableService('relaxed:attachment', 'GET');
+
     $file_contents = file_get_contents($this->files['1']->getFileUri());
     $encoded_digest = base64_encode(md5($file_contents));
 
@@ -195,7 +204,9 @@ class AttachmentResourceTest extends ResourceTestBase {
   }
 
   public function testPut() {
-    $serializer = $this->container->get('replication.serializer');
+    $this->enableService('relaxed:attachment', 'PUT');
+
+    $serializer = $this->container->get('serializer');
     $file_uri = 'public://new_example.txt';
     file_put_contents($file_uri, $this->randomMachineName());
     $file_stub = File::create(['uri' => $file_uri]);
@@ -209,7 +220,7 @@ class AttachmentResourceTest extends ResourceTestBase {
     $this->assertTrue(isset($data['rev']), 'PUT request returned a revision hash.');
 
     /** @var \Drupal\file\FileInterface $file */
-    $files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $file_uri]);
+    $files = \Drupal::entityManager()->getStorage('file')->loadByProperties(['uri' => $file_uri]);
     $file = reset($files);
     $this->assertTrue(!empty($file), 'File was saved.');
     $this->assertEqual($file->getFileUri(), $file_uri, 'File was saved with the correct URI.');
@@ -219,6 +230,8 @@ class AttachmentResourceTest extends ResourceTestBase {
   }
 
   public function testDelete() {
+    $this->enableService('relaxed:attachment', 'DELETE');
+
     $field_name = 'field_test_file';
     $attachment_info = $field_name . '/1/' . $this->files['2']->uuid() . '/public/' . $this->files['2']->getFileName();
     $response = $this->httpRequest("$this->dbname/" . $this->entity->uuid() . "/$attachment_info", 'DELETE', NULL);
diff --git a/src/Tests/BulkDocsResourceTest.php b/src/Tests/BulkDocsResourceTest.php
index cd01b8a..92f0e77 100644
--- a/src/Tests/BulkDocsResourceTest.php
+++ b/src/Tests/BulkDocsResourceTest.php
@@ -12,13 +12,14 @@ use Drupal\Component\Serialization\Json;
 class BulkDocsResourceTest extends ResourceTestBase {
 
   public function testPostCreate() {
-    $entity_types = ['entity_test_rev'];
+    $this->enableService('relaxed:bulk_docs', 'POST');
 
+    $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'create');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful post relaxed:bulk_docs';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -39,15 +40,16 @@ class BulkDocsResourceTest extends ResourceTestBase {
   }
 
   public function testPostUpdate() {
+    $this->enableService('relaxed:bulk_docs', 'POST');
     /** @var \Symfony\Component\Serializer\SerializerInterface $serializer */
-    $serializer = $this->container->get('replication.serializer');
+    $serializer = $this->container->get('serializer');
 
     $entity_type = 'entity_test_rev';
 
     // Create a user with the correct permissions.
     $permissions = $this->entityPermissions($entity_type, 'update');
     $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform push replication';
+    $permissions[] = 'restful post relaxed:bulk_docs';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
diff --git a/src/Tests/ChangesResourceTest.php b/src/Tests/ChangesResourceTest.php
index b55b4b1..4478b2b 100644
--- a/src/Tests/ChangesResourceTest.php
+++ b/src/Tests/ChangesResourceTest.php
@@ -12,11 +12,13 @@ use Drupal\Component\Serialization\Json;
 class ChangesResourceTest extends ResourceTestBase {
 
   public function testGet() {
-    $serializer = \Drupal::service('replication.serializer');
+    $serializer = \Drupal::service('serializer');
+    $this->enableService('relaxed:changes', 'GET');
 
     // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('workspace', 'view');
     $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform pull replication';
+    $permissions[] = 'restful get relaxed:changes';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
diff --git a/src/Tests/DbResourceTest.php b/src/Tests/DbResourceTest.php
index ef76d1b..3d599c7 100644
--- a/src/Tests/DbResourceTest.php
+++ b/src/Tests/DbResourceTest.php
@@ -13,30 +13,40 @@ class DbResourceTest extends ResourceTestBase {
 
   public function testHead() {
     // HEAD and GET is handled by the same resource.
+    $this->enableService('relaxed:db', 'GET');
+
+    // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('workspace', 'view');
+    $permissions[] = 'restful get relaxed:db';
+    $account = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($account);
+
+    $response = $this->httpRequest($this->dbname, 'HEAD', NULL);
+    $this->assertResponse('200', 'HTTP response code is correct.');
+    $this->assertHeader('content-type', $this->defaultMimeType);
+    $this->assertTrue(empty($response), 'HEAD request returned no body.');
 
     // Create a user with the 'perform pull replication' permission and test the
     // response code. It should be 200.
     $account = $this->drupalCreateUser(['perform pull replication']);
     $this->drupalLogin($account);
-    $this->httpRequest($this->dbname, 'HEAD', NULL);
+    $this->httpRequest($this->dbname, 'GET', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
-    $this->assertHeader('content-type', $this->defaultMimeType);
-    $this->assertTrue(empty($response), 'HEAD request returned no body.');
 
     // Create a user with the 'perform push replication' permission and test the
     // response code. It should be 200.
     $account = $this->drupalCreateUser(['perform push replication']);
     $this->drupalLogin($account);
-    $this->httpRequest($this->dbname, 'HEAD', NULL);
+    $this->httpRequest($this->dbname, 'GET', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
-    $this->assertHeader('content-type', $this->defaultMimeType);
-    $this->assertTrue(empty($response), 'HEAD request returned no body.');
   }
 
   public function testGet() {
+    $this->enableService('relaxed:db', 'GET');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform pull replication';
+    $permissions = $this->entityPermissions('workspace', 'view');
+    $permissions[] = 'restful get relaxed:db';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
@@ -70,9 +80,11 @@ class DbResourceTest extends ResourceTestBase {
   }
 
   public function testPut() {
+    $this->enableService('relaxed:db', 'PUT');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform push replication';
+    $permissions = $this->entityPermissions('workspace', 'create');
+    $permissions[] = 'restful put relaxed:db';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
@@ -116,14 +128,14 @@ class DbResourceTest extends ResourceTestBase {
   }
 
   public function testPost() {
-    $serializer = $this->container->get('replication.serializer');
+    $this->enableService('relaxed:db', 'POST');
+    $serializer = $this->container->get('serializer');
 
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'create');
-      $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful post relaxed:db';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -159,9 +171,11 @@ class DbResourceTest extends ResourceTestBase {
   }
 
   public function testDelete() {
+    $this->enableService('relaxed:db', 'DELETE');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform push replication';
+    $permissions = $this->entityPermissions('workspace', 'delete');
+    $permissions[] = 'restful delete relaxed:db';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
diff --git a/src/Tests/DocResourceTest.php b/src/Tests/DocResourceTest.php
index 8ba3764..340962a 100644
--- a/src/Tests/DocResourceTest.php
+++ b/src/Tests/DocResourceTest.php
@@ -17,12 +17,13 @@ class DocResourceTest extends ResourceTestBase {
 
   public function testHead() {
     // HEAD and GET is handled by the same resource.
+    $this->enableService('relaxed:doc', 'GET');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'view');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform pull replication';
+      $permissions[] = 'restful get relaxed:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -77,12 +78,13 @@ class DocResourceTest extends ResourceTestBase {
    * Tests non-multipart GET requests.
    */
   public function testGet() {
+    $this->enableService('relaxed:doc', 'GET');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'view');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform pull replication';
+      $permissions[] = 'restful get relaxed:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -146,12 +148,13 @@ class DocResourceTest extends ResourceTestBase {
    * Tests GET requests with multiple parts.
    */
   public function testGetOpenRevs() {
+    $this->enableService('relaxed:doc', 'GET', 'mixed');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'view');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform pull replication';
+      $permissions[] = 'restful get relaxed:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -214,13 +217,14 @@ class DocResourceTest extends ResourceTestBase {
   }
 
   public function testPut() {
-    $serializer = $this->container->get('replication.serializer');
+    $this->enableService('relaxed:doc', 'PUT');
+    $serializer = $this->container->get('serializer');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'create');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful put relaxed:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -265,12 +269,13 @@ class DocResourceTest extends ResourceTestBase {
   }
 
   public function testDelete() {
+    $this->enableService('relaxed:doc', 'DELETE');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'delete');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful delete relaxed:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -325,13 +330,14 @@ class DocResourceTest extends ResourceTestBase {
   }
 
   public function testStub() {
-    $serializer = $this->container->get('replication.serializer');
+    $this->enableService('relaxed:doc', 'PUT');
+    $serializer = $this->container->get('serializer');
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type_id) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type_id, 'create');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful put relaxed:doc';
       $permissions[] = 'administer users';
       $permissions[] = 'administer taxonomy';
       $account = $this->drupalCreateUser($permissions);
diff --git a/src/Tests/EnsureFullCommitResourceTest.php b/src/Tests/EnsureFullCommitResourceTest.php
index ab17164..1f23196 100644
--- a/src/Tests/EnsureFullCommitResourceTest.php
+++ b/src/Tests/EnsureFullCommitResourceTest.php
@@ -12,9 +12,10 @@ use Drupal\Component\Serialization\Json;
 class EnsureFullCommitResourceTest extends ResourceTestBase {
 
   public function testPost() {
+    $this->enableService('relaxed:ensure_full_commit', 'POST');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform push replication';
+    $permissions[] = 'restful post relaxed:ensure_full_commit';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
@@ -34,6 +35,13 @@ class EnsureFullCommitResourceTest extends ResourceTestBase {
     $this->drupalLogin($account);
     $this->httpRequest("$this->dbname/_ensure_full_commit", 'POST', NULL);
     $this->assertResponse('403', 'HTTP response code is correct.');
+
+    // Create a user with the 'perform push replication' permission and test the
+    // response code. It should be 201.
+    $account = $this->drupalCreateUser(['perform push replication']);
+    $this->drupalLogin($account);
+    $this->httpRequest("$this->dbname/_ensure_full_commit", 'POST', NULL);
+    $this->assertResponse('201', 'HTTP response code is correct.');
   }
 
 }
diff --git a/src/Tests/Entity/EntityAccessTest.php b/src/Tests/Entity/EntityAccessTest.php
index f3dabb8..f97051f 100644
--- a/src/Tests/Entity/EntityAccessTest.php
+++ b/src/Tests/Entity/EntityAccessTest.php
@@ -18,6 +18,7 @@ class EntityAccessTest extends WebTestBase  {
     'entity_test',
     'file',
     'multiversion',
+    'rest',
     'relaxed',
     'relaxed_test'
   ];
diff --git a/src/Tests/LocalDocResourceTest.php b/src/Tests/LocalDocResourceTest.php
index 73808a0..6182379 100644
--- a/src/Tests/LocalDocResourceTest.php
+++ b/src/Tests/LocalDocResourceTest.php
@@ -11,13 +11,13 @@ class LocalDocResourceTest extends ResourceTestBase {
 
   public function testHead() {
     // HEAD and GET is handled by the same resource.
+    $this->enableService('relaxed:local:doc', 'GET');
     $entity_types = ['entity_test_local'];
-
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'view');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform pull replication';
+      $permissions[] = 'restful get relaxed:local:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -40,12 +40,13 @@ class LocalDocResourceTest extends ResourceTestBase {
   }
 
   public function testGet() {
+    $this->enableService('relaxed:local:doc', 'GET');
     $entity_types = ['entity_test_local'];
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'view');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform pull replication';
+      $permissions[] = 'restful get relaxed:local:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -67,15 +68,14 @@ class LocalDocResourceTest extends ResourceTestBase {
   }
 
   public function testPut() {
-    $serializer = $this->container->get('replication.serializer');
-
+    $this->enableService('relaxed:local:doc', 'PUT');
+    $serializer = $this->container->get('serializer');
     $entity_types = ['entity_test_local'];
-
     foreach ($entity_types as $entity_type) {
       // Create a user with the correct permissions.
       $permissions = $this->entityPermissions($entity_type, 'create');
       $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
+      $permissions[] = 'restful put relaxed:local:doc';
       $account = $this->drupalCreateUser($permissions);
       $this->drupalLogin($account);
 
@@ -88,23 +88,10 @@ class LocalDocResourceTest extends ResourceTestBase {
       $this->assertResponse('201', 'HTTP response code is correct');
     }
 
-    // Create a user with the correct permissions.
-    $permissions = $this->entityPermissions('entity_test_rev', 'create');
-    $permissions[] = 'administer workspaces';
-    $permissions[] = 'perform push replication';
-    $account = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($account);
-
     // Test with an entity type that is not local.
     $entity = $this->entityTypeManager->getStorage('entity_test_rev')->create();
     $serialized = $serializer->serialize($entity, $this->defaultFormat);
-    $response = $this->httpRequest("$this->dbname/_local/" . $entity->uuid(), 'PUT', $serialized);
-
-    $expected = [
-      'error' => 'bad_request',
-      'reason' => 'This endpoint only supports local entity types.',
-    ];
-    $this->assertEqual($serializer->serialize($expected, 'json'), $response);
+    $this->httpRequest("$this->dbname/_local/" . $entity->uuid(), 'PUT', $serialized);
     $this->assertHeader('content-type', $this->defaultMimeType);
     $this->assertResponse('400', 'HTTP response code is correct.');
   }
diff --git a/src/Tests/RemoteConfigurationTest.php b/src/Tests/RemoteConfigurationTest.php
index 0c2ce6c..2c56f40 100644
--- a/src/Tests/RemoteConfigurationTest.php
+++ b/src/Tests/RemoteConfigurationTest.php
@@ -20,6 +20,7 @@ class RemoteConfigurationTest extends WebTestBase {
   public static $modules = [
     'file',
     'multiversion',
+    'rest',
     'relaxed',
     'relaxed_test'
   ];
diff --git a/src/Tests/ResourceTestBase.php b/src/Tests/ResourceTestBase.php
index f513dc7..1e630d9 100644
--- a/src/Tests/ResourceTestBase.php
+++ b/src/Tests/ResourceTestBase.php
@@ -4,18 +4,20 @@ namespace Drupal\relaxed\Tests;
 
 use Drupal\multiversion\Entity\Workspace;
 use Drupal\rest\Tests\RESTTestBase;
-use Drupal\simpletest\WebTestBase;
 
-abstract class ResourceTestBase extends WebTestBase {
+abstract class ResourceTestBase extends RESTTestBase {
 
   public static $modules = [
     'entity_test',
     'file',
     'multiversion',
+    'rest',
     'relaxed',
-    'relaxed_test',
+    'relaxed_test'
   ];
 
+  protected $strictConfigSchema = FALSE;
+
   /**
    * @var string
    */
@@ -82,6 +84,28 @@ abstract class ResourceTestBase extends WebTestBase {
     $this->revTree = $this->container->get('multiversion.entity_index.rev.tree');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function entityPermissions($entity_type, $operation) {
+    $return = parent::entityPermissions($entity_type, $operation);
+
+    // Extending with further entity types.
+    if (!$return) {
+      if (in_array($entity_type, ['entity_test_rev', 'entity_test_local'])) {
+        switch ($operation) {
+          case 'view':
+            return ['view test entity'];
+          case 'create':
+          case 'update':
+          case 'delete':
+            return ['administer entity_test content'];
+        }
+      }
+    }
+    return $return;
+  }
+
   /**
    * {@inheritdoc}
    *
@@ -96,18 +120,13 @@ abstract class ResourceTestBase extends WebTestBase {
     if ($mime_type === NULL) {
       $mime_type = $this->defaultMimeType;
     }
-
     if ($mime_type === $this->defaultMimeType && !isset($query['_format'])) {
       $query['_format'] = $this->defaultFormat;
     }
-
-    if (!in_array($method, ['GET', 'HEAD'])) {
+    if (!in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'])) {
       // GET the CSRF token first for writing requests.
-      // The '/rest/session/token' route has been deprecated in favour of the
-      // generic system route.
-      $token = $this->drupalGet('session/token');
+      $token = $this->drupalGet('rest/session/token');
     }
-
     $additional_headers = [];
     if (is_array($headers)) {
       foreach ($headers as $name => $value) {
@@ -237,27 +256,6 @@ abstract class ResourceTestBase extends WebTestBase {
     return $response;
   }
 
-  /**
-   * {@inheritdoc}
-   *
-   * This method is overridden to deal with a cURL quirk: the usage of
-   * CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to
-   * override it every time it is omitted.
-   */
-  protected function curlExec($curl_options, $redirect = FALSE) {
-    unset($this->response);
-
-    if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) {
-      if (!empty($curl_options[CURLOPT_HTTPGET])) {
-        $curl_options[CURLOPT_CUSTOMREQUEST] = 'GET';
-      }
-      if (!empty($curl_options[CURLOPT_POST])) {
-        $curl_options[CURLOPT_CUSTOMREQUEST] = 'POST';
-      }
-    }
-    return parent::curlExec($curl_options, $redirect);
-  }
-
   /**
    * Creates a custom workspace entity.
    */
@@ -265,22 +263,4 @@ abstract class ResourceTestBase extends WebTestBase {
     return workspace::create(['machine_name' => $name, 'label' => ucfirst($name), 'type' => 'basic']);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function entityPermissions($entity_type, $operation) {
-    if (in_array($entity_type, array('entity_test_rev', 'entity_test_local'))) {
-      switch ($operation) {
-        case 'view':
-          return array('view test entity');
-        case 'create':
-        case 'update':
-        case 'delete':
-          return array('administer entity_test content');
-      }
-    }
-
-    return [];
-  }
-
 }
diff --git a/src/Tests/RevsDiffResourceTest.php b/src/Tests/RevsDiffResourceTest.php
index 517bbe0..16630b5 100644
--- a/src/Tests/RevsDiffResourceTest.php
+++ b/src/Tests/RevsDiffResourceTest.php
@@ -12,20 +12,21 @@ use Drupal\Component\Serialization\Json;
 class RevsDiffResourceTest extends ResourceTestBase {
 
   public function testPostNoMissingRevisions() {
+    $this->enableService('relaxed:revs_diff', 'POST');
+
+    // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('workspace', 'view');
+    $permissions[] = 'administer workspaces';
+    $permissions[] = 'restful post relaxed:revs_diff';
+    $account = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($account);
+
+    // We set this here just to test creation and saving
+    // (with 'relaxed:revs_diff') the entity on the same workspace.
+    $this->multiversionManager->setActiveWorkspaceId($this->workspace->id());
+
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
-      // Create a user with the correct permissions.
-      $permissions = $this->entityPermissions($entity_type, 'create');
-      $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
-      $account = $this->drupalCreateUser($permissions);
-      $this->drupalLogin($account);
-
-      // We set this here just to test creation and saving (with 'revs_diff')
-      // the entity on the same workspace. Set this after the user has been
-      // created and logged in.
-      $this->multiversionManager->setActiveWorkspaceId($this->workspace->id());
-
       // Create a new test entity.
       $entity = $this->entityTypeManager->getStorage($entity_type)->create();
       $entity->save();
@@ -57,14 +58,16 @@ class RevsDiffResourceTest extends ResourceTestBase {
   }
 
   public function testPostMissingRevisions() {
+    $this->enableService('relaxed:revs_diff', 'POST');
+
+    // Create a user with the correct permissions.
+    $permissions = $this->entityPermissions('workspace', 'view');
+    $permissions[] = 'restful post relaxed:revs_diff';
+    $account = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($account);
+
     $entity_types = ['entity_test_rev'];
     foreach ($entity_types as $entity_type) {
-      // Create a user with the correct permissions.
-      $permissions = $this->entityPermissions($entity_type, 'create');
-      $permissions[] = 'administer workspaces';
-      $permissions[] = 'perform push replication';
-      $account = $this->drupalCreateUser($permissions);
-      $this->drupalLogin($account);
 
       // Create a new test entity.
       $entity = $this->entityTypeManager->getStorage($entity_type)->create();
diff --git a/src/Tests/RootResourceTest.php b/src/Tests/RootResourceTest.php
index 6457350..c52e579 100644
--- a/src/Tests/RootResourceTest.php
+++ b/src/Tests/RootResourceTest.php
@@ -12,8 +12,10 @@ use Drupal\Component\Serialization\Json;
 class RootResourceTest extends ResourceTestBase {
 
   public function testGet() {
+    $this->enableService('relaxed:root', 'GET');
+
     // Create a user with the correct permissions.
-    $permissions[] = 'perform pull replication';
+    $permissions[] = 'restful get relaxed:root';
     $account = $this->drupalCreateUser($permissions);
     $this->drupalLogin($account);
 
diff --git a/src/Tests/SessionResourceTest.php b/src/Tests/SessionResourceTest.php
index 4f2ebff..7e71618 100644
--- a/src/Tests/SessionResourceTest.php
+++ b/src/Tests/SessionResourceTest.php
@@ -12,20 +12,14 @@ use Drupal\Component\Serialization\Json;
 class SessionResourceTest extends ResourceTestBase {
 
   public function testGet() {
+    $this->enableService('relaxed:session', 'GET');
+
     // Create a user with the correct permissions and admin role.
-    $permissions = [
-      'administer permissions',
-      'administer users',
-    ];
+    $permissions = ['restful get relaxed:session', 'administer permissions', 'administer users'];
     $account = $this->drupalCreateUser($permissions, 'test_admin_user');
     $roles = $account->getRoles();
-
-    \Drupal::entityTypeManager()->getStorage('user_role')
-      ->load($roles[1])
-      ->setIsAdmin(TRUE)
-      ->save();
-
     $this->drupalLogin($account);
+    \Drupal::configFactory()->getEditable('user.settings')->set('admin_role', $roles[1])->save();
 
     $response = $this->httpRequest('_session', 'GET', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
@@ -50,50 +44,40 @@ class SessionResourceTest extends ResourceTestBase {
     // Logout the test_admin_user user.
     $this->drupalLogout();
 
-    // Create a user with the 'perform pull replication' permission and test the
-    // response code. It should be 200.
-    $account = $this->drupalCreateUser(['perform pull replication'], 'test_user_pull');
+    // Create a simple user with the correct permissions (no admin role).
+    $permissions = ['restful get relaxed:session'];
+    $account = $this->drupalCreateUser($permissions, 'test_user');
     $roles = $account->getRoles();
+    $this->drupalLogin($account);
 
-    $roles = [
-      'authenticated',
-      $roles[1],
-    ];
+    $response = $this->httpRequest('_session', 'GET', NULL);
+    $this->assertResponse('200', 'HTTP response code is correct.');
+    $this->assertHeader('content-type', $this->defaultMimeType);
+    $data = Json::decode($response);
 
     $expected = [
       'info' => [],
       'ok' => TRUE,
       'userCtx' => [
-        'user' => 'test_user_pull',
+        'user' => 'test_user',
         'roles' => $roles,
       ],
     ];
+    $this->assertIdentical($expected, $data, ('Correct values in response.'));
 
+    // Create a user with the 'perform pull replication' permission and test the
+    // response code. It should be 200.
+    $account = $this->drupalCreateUser(['perform pull replication']);
     $this->drupalLogin($account);
-    $response = $this->httpRequest('_session', 'GET', NULL);
-    $data = Json::decode($response);
+    $this->httpRequest('_session', 'GET', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
-    $this->assertIdentical($expected, $data, ('Correct values in response.'));
 
     // Create a user with the 'perform push replication' permission and test the
     // response code. It should be 200.
-    $account = $this->drupalCreateUser(['perform push replication'], 'test_user_push');
-    $roles = $account->getRoles();
-
-    $expected = [
-      'info' => [],
-      'ok' => TRUE,
-      'userCtx' => [
-        'user' => 'test_user_push',
-        'roles' => $roles,
-      ],
-    ];
-
+    $account = $this->drupalCreateUser(['perform push replication']);
     $this->drupalLogin($account);
-    $response = $this->httpRequest('_session', 'GET', NULL);
-    $data = Json::decode($response);
+    $this->httpRequest('_session', 'GET', NULL);
     $this->assertResponse('200', 'HTTP response code is correct.');
-    $this->assertIdentical($expected, $data, ('Correct values in response.'));
   }
 
 }
diff --git a/src/Tests/StubTest.php b/src/Tests/StubTest.php
index aaa7bec..bfcca35 100644
--- a/src/Tests/StubTest.php
+++ b/src/Tests/StubTest.php
@@ -45,7 +45,7 @@ class StubTest extends WebTestBase {
     $entity_type_id = 'entity_test_mulrev';
 
     /** @var \Symfony\Component\Serializer\SerializerInterface $serializer */
-    $serializer = \Drupal::service('replication.serializer');
+    $serializer = \Drupal::service('serializer');
 
     /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
     $entity_type_manager = \Drupal::service('entity_type.manager');
diff --git a/tests/bin/replication.sh b/tests/bin/replication.sh
index e206a77..991260b 100644
--- a/tests/bin/replication.sh
+++ b/tests/bin/replication.sh
@@ -14,7 +14,4 @@ php ~/drush.phar --yes --uri=http://localhost:8081 pm-enable entity_test, relaxe
 # Check CouchDB installation.
 curl http://localhost:${COUCH_PORT}
 
-# Check if CouchDB is running.
-curl http://127.0.0.1:5984
-
 vendor/phpunit/phpunit/phpunit --verbose --debug --configuration phpunit.travis.xml --bootstrap core/tests/bootstrap.php
diff --git a/tests/phpunit.travis.xml b/tests/phpunit.travis.xml
index 118a6e4..1e0d3a5 100644
--- a/tests/phpunit.travis.xml
+++ b/tests/phpunit.travis.xml
@@ -19,6 +19,8 @@
     <env name="SIMPLETEST_BASE_URL" value="http://localhost:8080"/>
     <!-- Example SIMPLETEST_DB value: mysql://username:password@localhost/databasename#table_prefix -->
     <env name="SIMPLETEST_DB" value="mysql://root@localhost/drupal0"/>
+    <!-- Example BROWSERTEST_OUTPUT_DIRECTORY value: /path/to/webroot/sites/simpletest/browser_output -->
+    <env name="BROWSERTEST_OUTPUT_DIRECTORY" value=""/>
   </php>
   <testsuites>
     <testsuite name="integration">
diff --git a/tests/src/Functional/RelaxedEncryptTest.php b/tests/src/Functional/RelaxedEncryptTest.php
new file mode 100644
index 0000000..979292d
--- /dev/null
+++ b/tests/src/Functional/RelaxedEncryptTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\Tests\relaxed\Functional;
+
+use Drupal\encrypt\Entity\EncryptionProfile;
+use Drupal\key\Entity\Key;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @group relaxed
+ */
+class RelaxedEncryptTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable for this test.
+   *
+   * @var string[]
+   */
+  public static $modules = [
+    'key',
+    'encrypt',
+    'encrypt_test',
+    'relaxed',
+    'replication',
+    'multiversion',
+    'key_value',
+    'serialization',
+    'rest',
+    'basic_auth'
+  ];
+
+  /**
+   * Tests encrypt integration.
+   */
+  public function testEncryptIntegration() {
+    // Create a 256bit testkey.
+    $key_256 = Key::create([
+      'id' => 'testing_key_256',
+      'label' => 'Testing Key 256 bit',
+      'key_type' => "encryption",
+      'key_type_settings' => ['key_size' => '256'],
+      'key_provider' => 'config',
+      'key_provider_settings' => ['key_value' => 'mustbesixteenbitmustbesixteenbit'],
+    ]);
+    $key_256->save();
+
+    // Create an Encrption Profile.
+    $encryption_profile = EncryptionProfile::create([
+      'id' => 'encryption_profile',
+      'label' => 'Encryption profile',
+      'encryption_method' => 'test_encryption_method',
+      'encryption_key' => 'testing_key_256',
+    ]);
+    $encryption_profile->save();
+
+    // Login as root.
+    $this->drupalLogin($this->rootUser);
+
+    // Add a replication username and password, enable encryption and set
+    // encryption profile.
+    $this->drupalGet('/admin/config/relaxed/settings');
+    $page = $this->getSession()->getPage();
+    $page->fillField('edit-username', 'replication_user');
+    $page->fillField('edit-password', 'replication_password');
+    $page->checkField('edit-encrypt');
+    $page->selectFieldOption('edit-encrypt-profile', 'encryption_profile');
+    $page->pressButton('Save configuration');
+    $this->assertSession()->pageTextContains('The configuration options have been saved.');
+
+    $config = \Drupal::config('relaxed.settings');
+    $transformer = \Drupal::service('relaxed.sensitive_data.transformer');
+    // Make sure the password is not stored in plain text.
+    $this->assertNotEquals('replication_password', $config->get('password'));
+    // Make sure the decrypted password matches the one entered.
+    $this->assertEquals('replication_password', $transformer->get($config->get('password')));
+  }
+
+}
diff --git a/tests/src/Integration/PhpReplicatorTest.php b/tests/src/Integration/PhpReplicatorTest.php
index 0b3ab16..f169f67 100644
--- a/tests/src/Integration/PhpReplicatorTest.php
+++ b/tests/src/Integration/PhpReplicatorTest.php
@@ -26,4 +26,4 @@ class PhpReplicatorTest extends ReplicationTestBase {
     $this->assertAllDocsNumber($this->couchdbUrl . '/' . $this->targetDb . '/_all_docs', 9);
   }
 
-}
+}
\ No newline at end of file
diff --git a/tests/src/Integration/ReplicationTestBase.php b/tests/src/Integration/ReplicationTestBase.php
index 3636525..d569120 100644
--- a/tests/src/Integration/ReplicationTestBase.php
+++ b/tests/src/Integration/ReplicationTestBase.php
@@ -47,6 +47,7 @@ abstract class ReplicationTestBase extends KernelTestBase {
   public static $modules = [
     'serialization',
     'system',
+    'rest',
     'key_value',
     'multiversion',
     'relaxed',
diff --git a/tests/src/Unit/FormatNegotiatorManagerTest.php b/tests/src/Unit/FormatNegotiatorManagerTest.php
deleted file mode 100644
index 127d6f7..0000000
--- a/tests/src/Unit/FormatNegotiatorManagerTest.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-
-namespace Drupal\Tests\relaxed\Unit;
-
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\multiversion\Entity\Workspace;
-use Drupal\relaxed\Plugin\FormatNegotiator\JsonNegotiator;
-use Drupal\relaxed\Plugin\FormatNegotiator\StreamNegotiator;
-
-/**
- * @coversDefaultClass \Drupal\relaxed\Plugin\FormatNegotiatorManager
- */
-class FormatNegotiatorManagerTest extends KernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = ['serialization', 'system', 'user', 'key_value', 'multiversion', 'relaxed', 'replication'];
-
-  /**
-   * @var \Drupal\relaxed\Plugin\FormatNegotiatorManagerInterface
-   */
-  protected $manager;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-
-    $this->installConfig(['multiversion', 'relaxed']);
-    $this->installEntitySchema('workspace');
-    // Create the default workspace because the multiversion_install() hook is
-    // not executed in unit tests.
-    Workspace::create(['machine_name' => 'default', 'type' => 'basic'])->save();
-
-    $this->manager = $this->container->get('plugin.manager.format_negotiator');
-  }
-
-  /**
-   * @covers ::availableFormats
-   */
-  public function testAvailableFormats() {
-    $expected = ['stream', 'base64_stream', 'json', 'mixed', 'related'];
-    $this->assertEquals($expected, $this->manager->availableFormats());
-  }
-
-  /**
-   * @covers ::select
-   *
-   * @dataProvider providerTestSelect
-   */
-  public function testSelect($expected_instance, $params) {
-    $this->assertInstanceOf($expected_instance, call_user_func_array([$this->manager, 'select'], $params));
-  }
-
-  /**
-   * Data provider for testSelect().
-   */
-  public function providerTestSelect() {
-    $data = [];
-
-    $data['json_request'] = [JsonNegotiator::class, ['json', 'post', 'request']];
-    $data['json_response'] = [JsonNegotiator::class, ['json', 'post', 'response']];
-    $data['mixed_request'] = [JsonNegotiator::class, ['mixed', 'post', 'request']];
-    $data['mixed_response'] = [JsonNegotiator::class, ['mixed', 'post', 'response']];
-    $data['related_request'] = [JsonNegotiator::class, ['related', 'post', 'request']];
-    $data['related_response'] = [JsonNegotiator::class, ['related', 'post', 'response']];
-
-    $data['stream_request'] = [StreamNegotiator::class, ['stream', 'get', 'request']];
-    $data['base64_stream_request'] = [StreamNegotiator::class, ['base64_stream', 'get', 'request']];
-    // For GET (and HEAD) requests, streaming is allowed in responses.
-    $data['stream_response'] = [StreamNegotiator::class, ['stream', 'get', 'response']];
-    $data['base64_stream_response'] = [StreamNegotiator::class, ['base64_stream', 'get', 'response']];
-
-    $data['stream_request'] = [StreamNegotiator::class, ['stream', 'post', 'request']];
-    $data['base64_stream_request'] = [StreamNegotiator::class, ['base64_stream', 'post', 'request']];
-    // Stream does not apply for POST responses, so JSON will be used.
-    $data['stream_response'] = [JsonNegotiator::class, ['stream', 'post', 'response']];
-    $data['base64_stream_response'] = [JsonNegotiator::class, ['base64_stream', 'post', 'response']];
-
-    return $data;
-  }
-
-  /**
-   * @covers ::getDefinitions
-   */
-  public function testGetDefinitions() {
-    // Test priority weighting of definitions. Stream should be ahead of JSON as
-    // it has a higher priority.
-    $this->assertEquals(['stream', 'json'], array_keys($this->manager->getDefinitions()));
-  }
-
-}
diff --git a/tests/src/Unit/RemoteTest.php b/tests/src/Unit/RemoteTest.php
index df238b7..75ee0cb 100644
--- a/tests/src/Unit/RemoteTest.php
+++ b/tests/src/Unit/RemoteTest.php
@@ -17,7 +17,7 @@ class RemoteTest extends KernelTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['serialization', 'system', 'user', 'key_value', 'multiversion', 'relaxed', 'replication'];
+  public static $modules = ['serialization', 'system', 'user', 'rest', 'key_value', 'multiversion', 'relaxed', 'replication'];
 
   /**
    * {@inheritdoc}
