diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js
index 2ad7a635be95eb0525f033a1e8de8eecafe9edb8..cde4ed57f250726a9a53cd65a92988ac04a88c91 100644
--- a/packages/component-faraday-selectors/src/index.js
+++ b/packages/component-faraday-selectors/src/index.js
@@ -56,10 +56,11 @@ export const getHERecommendation = (state, collectionId, fragmentId) => {
   )
 }
 
+const cantMakeDecisionStatuses = ['rejected', 'published', 'draft']
 export const canMakeDecision = (state, collection) => {
   const status = get(collection, 'status')
 
-  if (!status || status === 'rejected' || status === 'published') return false
+  if (!status || cantMakeDecisionStatuses.includes(status)) return false
 
   const isEIC = currentUserIs(state, 'adminEiC')
   return isEIC && status
@@ -72,3 +73,11 @@ export const canSeeReviewersReports = (state, collectionId) => {
 }
 
 export const canSeeEditorialComments = canSeeReviewersReports
+
+export const canMakeRevision = (state, collection) => {
+  const currentUserId = get(state, 'currentUser.user.id')
+  return (
+    collection.status === 'revisionRequested' &&
+    collection.owners.map(o => o.id).includes(currentUserId)
+  )
+}
diff --git a/packages/component-fixture-manager/.gitignore b/packages/component-fixture-manager/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18
--- /dev/null
+++ b/packages/component-fixture-manager/.gitignore
@@ -0,0 +1,8 @@
+_build/
+api/
+logs/
+node_modules/
+uploads/
+.env.*
+.env
+config/local*.*
\ No newline at end of file
diff --git a/packages/component-fixture-manager/README.md b/packages/component-fixture-manager/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a0ac7afbf2c8255c8a94f4f2ff525dd245cc3c93
--- /dev/null
+++ b/packages/component-fixture-manager/README.md
@@ -0,0 +1,2 @@
+# Helper Service
+
diff --git a/packages/component-fixture-manager/index.js b/packages/component-fixture-manager/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd95e77d96237d61fc1a8f03d5f2d7d2f80a7e0f
--- /dev/null
+++ b/packages/component-fixture-manager/index.js
@@ -0,0 +1 @@
+module.exports = require('./src/Fixture')
diff --git a/packages/component-fixture-manager/package.json b/packages/component-fixture-manager/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..fe566ea163b877842eba597a058293b0a0af7fbe
--- /dev/null
+++ b/packages/component-fixture-manager/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "pubsweet-component-fixture-service",
+  "version": "0.0.1",
+  "description": "fixture service component for pubsweet",
+  "license": "MIT",
+  "author": "Collaborative Knowledge Foundation",
+  "files": [
+    "src"
+  ],
+  "main": "index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://gitlab.coko.foundation/xpub/xpub-faraday",
+    "path": "component-fixture-service"
+  },
+  "dependencies": {
+  },
+  "peerDependencies": {
+    "@pubsweet/logger": "^0.0.1",
+    "pubsweet-server": "^1.0.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}
diff --git a/packages/component-fixture-manager/src/Fixture.js b/packages/component-fixture-manager/src/Fixture.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2c53d05d677167696b1dad66e7b1f294a23ae10
--- /dev/null
+++ b/packages/component-fixture-manager/src/Fixture.js
@@ -0,0 +1,7 @@
+const fixtures = require('./fixtures/fixtures')
+const Model = require('./helpers/Model')
+
+module.exports = {
+  fixtures,
+  Model,
+}
diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js
new file mode 100644
index 0000000000000000000000000000000000000000..0633f816e39359721e73dd32252279f05bcdff5e
--- /dev/null
+++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js
@@ -0,0 +1,8 @@
+const Chance = require('chance')
+
+const chance = new Chance()
+const collId = chance.guid()
+
+module.exports = {
+  standardCollID: collId,
+}
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js
similarity index 65%
rename from packages/component-manuscript-manager/src/tests/fixtures/collections.js
rename to packages/component-fixture-manager/src/fixtures/collections.js
index bef6132199981a79b87ea86440146eaab5891883..ead1ed38cabd8e5eaa615a1fa7a9e5b84d6d3052 100644
--- a/packages/component-manuscript-manager/src/tests/fixtures/collections.js
+++ b/packages/component-fixture-manager/src/fixtures/collections.js
@@ -1,29 +1,17 @@
 const Chance = require('chance')
-const {
-  user,
-  handlingEditor,
-  author,
-  reviewer,
-  answerReviewer,
-} = require('./userData')
+const { user, handlingEditor, answerHE } = require('./userData')
 const { fragment } = require('./fragments')
+const { standardCollID } = require('./collectionIDs')
 
 const chance = new Chance()
 const collections = {
   collection: {
-    id: chance.guid(),
+    id: standardCollID,
     title: chance.sentence(),
     type: 'collection',
     fragments: [fragment.id],
     owners: [user.id],
     save: jest.fn(),
-    authors: [
-      {
-        userId: author.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
     invitations: [
       {
         id: chance.guid(),
@@ -36,19 +24,10 @@ const collections = {
       },
       {
         id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: reviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
+        role: 'handlingEditor',
         hasAnswer: true,
         isAccepted: false,
-        userId: answerReviewer.id,
+        userId: answerHE.id,
         invitedOn: chance.timestamp(),
         respondedOn: chance.timestamp(),
       },
diff --git a/packages/component-invite/src/tests/fixtures/fixtures.js b/packages/component-fixture-manager/src/fixtures/fixtures.js
similarity index 100%
rename from packages/component-invite/src/tests/fixtures/fixtures.js
rename to packages/component-fixture-manager/src/fixtures/fixtures.js
diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8a30a48644694fb9a747f11ec7dffbc3230059b
--- /dev/null
+++ b/packages/component-fixture-manager/src/fixtures/fragments.js
@@ -0,0 +1,119 @@
+const Chance = require('chance')
+const {
+  submittingAuthor,
+  reviewer,
+  answerReviewer,
+  recReviewer,
+  handlingEditor,
+  admin,
+} = require('./userData')
+const { standardCollID } = require('./collectionIDs')
+const { user } = require('./userData')
+
+const chance = new Chance()
+const fragments = {
+  fragment: {
+    id: chance.guid(),
+    collectionId: standardCollID,
+    metadata: {
+      title: chance.sentence(),
+      abstract: chance.paragraph(),
+    },
+    recommendations: [
+      {
+        recommendation: 'publish',
+        recommendationType: 'review',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: recReviewer.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+      {
+        recommendation: 'minor',
+        recommendationType: 'editorRecommendation',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: handlingEditor.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+      {
+        recommendation: 'publish',
+        recommendationType: 'editorRecommendation',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: admin.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+    ],
+    authors: [
+      {
+        id: submittingAuthor.id,
+        isSubmitting: true,
+        isCorresponding: false,
+      },
+    ],
+    invitations: [
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: false,
+        isAccepted: false,
+        userId: reviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: null,
+      },
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: true,
+        isAccepted: false,
+        userId: answerReviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: chance.timestamp(),
+      },
+    ],
+    save: jest.fn(() => fragments.fragment),
+    owners: [user.id],
+  },
+}
+
+module.exports = fragments
diff --git a/packages/component-user-manager/src/tests/fixtures/teamIDs.js b/packages/component-fixture-manager/src/fixtures/teamIDs.js
similarity index 79%
rename from packages/component-user-manager/src/tests/fixtures/teamIDs.js
rename to packages/component-fixture-manager/src/fixtures/teamIDs.js
index f8cfc51464701c2ab93b312dd73ed3ea5e2f7ea3..83ce3a556417bc4650efdc9ff478b8bad64a9f13 100644
--- a/packages/component-user-manager/src/tests/fixtures/teamIDs.js
+++ b/packages/component-fixture-manager/src/fixtures/teamIDs.js
@@ -2,9 +2,11 @@ const Chance = require('chance')
 
 const chance = new Chance()
 const heID = chance.guid()
+const revId = chance.guid()
 const authorID = chance.guid()
 
 module.exports = {
   heTeamID: heID,
+  revTeamID: revId,
   authorTeamID: authorID,
 }
diff --git a/packages/component-invite/src/tests/fixtures/teams.js b/packages/component-fixture-manager/src/fixtures/teams.js
similarity index 59%
rename from packages/component-invite/src/tests/fixtures/teams.js
rename to packages/component-fixture-manager/src/fixtures/teams.js
index 7e4611da3489cceb1308e9ffaf072a85866f4ca2..84d784b4b4be4c38a4757fccada612705bc8b1b1 100644
--- a/packages/component-invite/src/tests/fixtures/teams.js
+++ b/packages/component-fixture-manager/src/fixtures/teams.js
@@ -1,8 +1,12 @@
 const users = require('./users')
 const collections = require('./collections')
-const { heTeamID, revTeamID } = require('./teamIDs')
+const fragments = require('./fragments')
+
+const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs')
+const { submittingAuthor } = require('./userData')
 
 const { collection } = collections
+const { fragment } = fragments
 const { handlingEditor, reviewer } = users
 const teams = {
   heTeam: {
@@ -29,13 +33,29 @@ const teams = {
     group: 'reviewer',
     name: 'reviewer',
     object: {
-      type: 'collection',
-      id: collection.id,
+      type: 'fragment',
+      id: fragment.id,
     },
     members: [reviewer.id],
     save: jest.fn(() => teams.revTeam),
     updateProperties: jest.fn(() => teams.revTeam),
     id: revTeamID,
   },
+  authorTeam: {
+    teamType: {
+      name: 'author',
+      permissions: 'author',
+    },
+    group: 'author',
+    name: 'author',
+    object: {
+      type: 'fragment',
+      id: fragment.id,
+    },
+    members: [submittingAuthor.id],
+    save: jest.fn(() => teams.authorTeam),
+    updateProperties: jest.fn(() => teams.authorTeam),
+    id: authorTeamID,
+  },
 }
 module.exports = teams
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/userData.js b/packages/component-fixture-manager/src/fixtures/userData.js
similarity index 86%
rename from packages/component-manuscript-manager/src/tests/fixtures/userData.js
rename to packages/component-fixture-manager/src/fixtures/userData.js
index 4e3b994a70b782f25ec85e0f41410824dd4307c4..9920552365178966754bf7681df61eaa7a15878c 100644
--- a/packages/component-manuscript-manager/src/tests/fixtures/userData.js
+++ b/packages/component-fixture-manager/src/fixtures/userData.js
@@ -15,5 +15,7 @@ module.exports = {
   author: generateUserData(),
   reviewer: generateUserData(),
   answerReviewer: generateUserData(),
+  submittingAuthor: generateUserData(),
   recReviewer: generateUserData(),
+  answerHE: generateUserData(),
 }
diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-fixture-manager/src/fixtures/users.js
similarity index 65%
rename from packages/component-invite/src/tests/fixtures/users.js
rename to packages/component-fixture-manager/src/fixtures/users.js
index c5f0a5e41f8bca54d62c6534ef263a6de77c99bc..29c50f9327b03f6551aca7cbcf48526446726a0f 100644
--- a/packages/component-invite/src/tests/fixtures/users.js
+++ b/packages/component-fixture-manager/src/fixtures/users.js
@@ -1,4 +1,4 @@
-const { heTeamID, revTeamID } = require('./teamIDs')
+const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs')
 const {
   handlingEditor,
   user,
@@ -6,6 +6,9 @@ const {
   author,
   reviewer,
   answerReviewer,
+  submittingAuthor,
+  recReviewer,
+  answerHE,
 } = require('./userData')
 const Chance = require('chance')
 
@@ -51,6 +54,21 @@ const users = {
     handlingEditor: true,
     title: 'Mr',
   },
+  answerHE: {
+    type: 'user',
+    username: chance.word(),
+    email: answerHE.email,
+    password: 'password',
+    admin: false,
+    id: answerHE.id,
+    firstName: answerHE.firstName,
+    lastName: answerHE.lastName,
+    teams: [heTeamID],
+    save: jest.fn(() => users.answerHE),
+    editorInChief: false,
+    handlingEditor: true,
+    title: 'Mr',
+  },
   user: {
     type: 'user',
     username: chance.word(),
@@ -65,6 +83,8 @@ const users = {
     title: 'Mr',
     save: jest.fn(() => users.user),
     isConfirmed: false,
+    updateProperties: jest.fn(() => users.user),
+    teams: [],
   },
   author: {
     type: 'user',
@@ -79,6 +99,8 @@ const users = {
     title: 'Mr',
     save: jest.fn(() => users.author),
     isConfirmed: true,
+    passwordResetToken: chance.hash(),
+    teams: [authorTeamID],
   },
   reviewer: {
     type: 'user',
@@ -112,6 +134,36 @@ const users = {
     teams: [revTeamID],
     invitationToken: 'inv-token-123',
   },
+  submittingAuthor: {
+    type: 'user',
+    username: 'sauthor',
+    email: submittingAuthor.email,
+    password: 'password',
+    admin: false,
+    id: submittingAuthor.id,
+    passwordResetToken: chance.hash(),
+    firstName: submittingAuthor.firstName,
+    lastName: submittingAuthor.lastName,
+    affiliation: chance.company(),
+    title: 'Mr',
+    save: jest.fn(() => users.submittingAuthor),
+    isConfirmed: false,
+  },
+  recReviewer: {
+    type: 'user',
+    username: chance.word(),
+    email: recReviewer.email,
+    password: 'password',
+    admin: false,
+    id: recReviewer.id,
+    firstName: recReviewer.firstName,
+    lastName: recReviewer.lastName,
+    affiliation: chance.company(),
+    title: 'Mr',
+    save: jest.fn(() => users.recReviewer),
+    isConfirmed: true,
+    teams: [revTeamID],
+  },
 }
 
 module.exports = users
diff --git a/packages/component-invite/src/tests/helpers/Model.js b/packages/component-fixture-manager/src/helpers/Model.js
similarity index 93%
rename from packages/component-invite/src/tests/helpers/Model.js
rename to packages/component-fixture-manager/src/helpers/Model.js
index df543155a679c4c7e2bb91a8304835c458e38d44..9ff60517c6f1ce111f22a5a4f1e5369737c0a9c6 100644
--- a/packages/component-invite/src/tests/helpers/Model.js
+++ b/packages/component-fixture-manager/src/helpers/Model.js
@@ -1,5 +1,3 @@
-// const fixtures = require('../fixtures/fixtures')
-
 const UserMock = require('../mocks/User')
 const TeamMock = require('../mocks/Team')
 
@@ -18,12 +16,17 @@ const build = fixtures => {
       find: jest.fn(id => findMock(id, 'fragments', fixtures)),
     },
   }
+
   UserMock.find = jest.fn(id => findMock(id, 'users', fixtures))
   UserMock.findByEmail = jest.fn(email => findByEmailMock(email, fixtures))
   UserMock.all = jest.fn(() => Object.values(fixtures.users))
   UserMock.findOneByField = jest.fn((field, value) =>
     findOneByFieldMock(field, value, 'users', fixtures),
   )
+  UserMock.updateProperties = jest.fn(user =>
+    updatePropertiesMock(user, 'users'),
+  )
+
   TeamMock.find = jest.fn(id => findMock(id, 'teams', fixtures))
   TeamMock.updateProperties = jest.fn(team =>
     updatePropertiesMock(team, 'teams', fixtures),
@@ -40,7 +43,7 @@ const findMock = (id, type, fixtures) => {
     fixtureObj => fixtureObj.id === id,
   )
 
-  if (foundObj === undefined) return Promise.reject(notFoundError)
+  if (!foundObj) return Promise.reject(notFoundError)
   return Promise.resolve(foundObj)
 }
 
diff --git a/packages/component-invite/src/tests/mocks/Team.js b/packages/component-fixture-manager/src/mocks/Team.js
similarity index 100%
rename from packages/component-invite/src/tests/mocks/Team.js
rename to packages/component-fixture-manager/src/mocks/Team.js
diff --git a/packages/component-invite/src/tests/mocks/User.js b/packages/component-fixture-manager/src/mocks/User.js
similarity index 100%
rename from packages/component-invite/src/tests/mocks/User.js
rename to packages/component-fixture-manager/src/mocks/User.js
diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js
index 6e502735b7ae9ce777bbda52be940b4e06abf953..51ab28826eec2d9b37eee43915cca6f494394df9 100644
--- a/packages/component-helper-service/src/services/Collection.js
+++ b/packages/component-helper-service/src/services/Collection.js
@@ -7,12 +7,27 @@ class Collection {
     this.collection = collection
   }
 
-  async updateStatusByRecommendation({ recommendation }) {
-    let newStatus = 'pendingApproval'
-    if (['minor', 'major'].includes(recommendation))
-      newStatus = 'revisionRequested'
+  async updateStatusByRecommendation({
+    recommendation,
+    isHandlingEditor = false,
+  }) {
+    let newStatus
+    if (isHandlingEditor) {
+      newStatus = 'pendingApproval'
+      if (['minor', 'major'].includes(recommendation)) {
+        newStatus = 'revisionRequested'
+      }
+    } else {
+      if (recommendation === 'minor') {
+        newStatus = 'reviewCompleted'
+      }
 
-    await this.updateStatus({ newStatus })
+      if (recommendation === 'major') {
+        newStatus = 'reviewersInvited'
+      }
+    }
+
+    this.updateStatus({ newStatus })
   }
 
   async updateFinalStatusByRecommendation({ recommendation }) {
@@ -36,42 +51,10 @@ class Collection {
 
   async updateStatus({ newStatus }) {
     this.collection.status = newStatus
-    this.collection.visibleStatus = statuses[this.collection.status].private
+    this.collection.visibleStatus = statuses[this.collection.status].public
     await this.collection.save()
   }
 
-  async getAuthorData({ UserModel }) {
-    const { collection: { authors } } = this
-    const submittingAuthorData = authors.find(
-      author => author.isSubmitting === true,
-    )
-    const submittingAuthor = await UserModel.find(submittingAuthorData.userId)
-    const authorsPromises = authors.map(async author => {
-      const user = await UserModel.find(author.userId)
-      return `${user.firstName} ${user.lastName}`
-    })
-    const authorsList = await Promise.all(authorsPromises)
-
-    return {
-      authorsList,
-      submittingAuthor,
-    }
-  }
-
-  getReviewerInvitations({ agree = true }) {
-    const { collection: { invitations } } = this
-    return agree
-      ? invitations.filter(
-          inv =>
-            inv.role === 'reviewer' &&
-            inv.hasAnswer === true &&
-            inv.isAccepted === true,
-        )
-      : invitations.filter(
-          inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-        )
-  }
-
   async addHandlingEditor({ user, invitation }) {
     this.collection.handlingEditor = {
       id: user.id,
diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js
index 49a106d8d72305dcf029accd2bd0a094a52cab45..7e7bcd4510219d5a712d0c71b0f2f05585f075ed 100644
--- a/packages/component-helper-service/src/services/Email.js
+++ b/packages/component-helper-service/src/services/Email.js
@@ -1,4 +1,4 @@
-const Collection = require('./Collection')
+const Fragment = require('./Fragment')
 const User = require('./User')
 const get = require('lodash/get')
 const config = require('config')
@@ -29,15 +29,17 @@ class Email {
     recommendation = {},
     isSubmitted = false,
     agree = false,
+    FragmentModel,
   }) {
     const {
       UserModel,
       collection,
-      parsedFragment: { recommendations, title, type },
+      parsedFragment: { recommendations, title, type, id },
       authors: { submittingAuthor: { firstName = '', lastName = '' } },
     } = this
-    const collectionHelper = new Collection({ collection })
-    const reviewerInvitations = collectionHelper.getReviewerInvitations({
+    const fragment = await FragmentModel.find(id)
+    const fragmentHelper = new Fragment({ fragment })
+    const reviewerInvitations = fragmentHelper.getReviewerInvitations({
       agree,
     })
 
@@ -98,7 +100,11 @@ class Email {
     )
   }
 
-  async setupAuthorsEmail({ requestToRevision = false, publish = false }) {
+  async setupAuthorsEmail({
+    requestToRevision = false,
+    publish = false,
+    FragmentModel,
+  }) {
     const {
       baseUrl,
       collection,
@@ -123,7 +129,8 @@ class Email {
         },
       ]
     } else {
-      toAuthors = collection.authors.map(author => ({
+      const fragment = await FragmentModel.find(id)
+      toAuthors = fragment.authors.map(author => ({
         email: author.email,
         name: `${author.firstName} ${author.lastName}`,
       }))
@@ -133,7 +140,7 @@ class Email {
         baseUrl,
         emailType,
         toEmail: toAuthor.email,
-        handlingEditorName: collection.handlingEditor.name,
+        handlingEditorName: get(collection, 'handlingEditor.name'),
         meta: {
           collection,
           authorNoteText,
@@ -150,6 +157,7 @@ class Email {
 
   async setupHandlingEditorEmail({
     publish = false,
+    returnWithComments = false,
     reviewSubmitted = false,
     reviewerName = '',
   }) {
@@ -157,16 +165,17 @@ class Email {
       baseUrl,
       UserModel,
       collection,
-      parsedFragment: { title, id },
+      parsedFragment: { eicComments = '', title, id },
       authors: { submittingAuthor: { firstName = '', lastName = '' } },
     } = this
     const userHelper = new User({ UserModel })
     const eic = await userHelper.getEditorInChief()
-    const toEmail = collection.handlingEditor.email
+    const toEmail = get(collection, 'handlingEditor.email')
     let emailType = publish
       ? 'he-manuscript-published'
       : 'he-manuscript-rejected'
     if (reviewSubmitted) emailType = 'review-submitted'
+    if (returnWithComments) emailType = 'he-manuscript-return-with-comments'
     mailService.sendNotificationEmail({
       toEmail,
       baseUrl,
@@ -174,8 +183,9 @@ class Email {
       meta: {
         collection,
         reviewerName,
+        eicComments,
         eicName: `${eic.firstName} ${eic.lastName}`,
-        handlingEditorName: collection.handlingEditor.name,
+        handlingEditorName: get(collection, 'handlingEditor.name') || '',
         emailSubject: `${collection.customId}: Manuscript Decision`,
         fragment: {
           id,
@@ -234,7 +244,7 @@ class Email {
         collection,
         fragment: { id },
         eicName: `${eic.firstName} ${eic.lastName}`,
-        handlingEditorName: collection.handlingEditor.name,
+        handlingEditorName: get(collection, 'handlingEditor.name'),
       },
     })
   }
@@ -322,7 +332,7 @@ class Email {
   }
 
   async setupReviewerUnassignEmail({ user, authorName }) {
-    const { collection, fragment: { title } } = this
+    const { collection, parsedFragment: { title = '' } } = this
 
     await mailService.sendNotificationEmail({
       toEmail: user.email,
diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js
index d4be48eec23c41fd8e08e2d5695a34a705454fb1..078aa44fa9202807bc264fb2fe2eebb6d355f931 100644
--- a/packages/component-helper-service/src/services/Fragment.js
+++ b/packages/component-helper-service/src/services/Fragment.js
@@ -1,13 +1,16 @@
+const get = require('lodash/get')
+
 class Fragment {
   constructor({ fragment }) {
     this.fragment = fragment
   }
+
   async getFragmentData({ handlingEditor = {} }) {
-    const { fragment: { metadata, recommendations = [], id } } = this
+    const { fragment: { metadata = {}, recommendations = [], id } } = this
     const heRecommendation = recommendations.find(
       rec => rec.userId === handlingEditor.id,
     )
-    let { title, abstract } = metadata
+    let { title = '', abstract = '' } = metadata
     const { type } = metadata
     title = title.replace(/<(.|\n)*?>/g, '')
     abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : ''
@@ -21,6 +24,72 @@ class Fragment {
       heRecommendation,
     }
   }
+
+  async addAuthor({ user, isSubmitting, isCorresponding }) {
+    const { fragment } = this
+    fragment.authors = fragment.authors || []
+    const author = {
+      id: user.id,
+      firstName: user.firstName || '',
+      lastName: user.lastName || '',
+      email: user.email,
+      title: user.title || '',
+      affiliation: user.affiliation || '',
+      isSubmitting,
+      isCorresponding,
+    }
+    fragment.authors.push(author)
+    await fragment.save()
+
+    return author
+  }
+
+  async getAuthorData({ UserModel }) {
+    const { fragment: { authors = [] } } = this
+    const submittingAuthorData = authors.find(author => author.isSubmitting)
+
+    try {
+      const submittingAuthor = await UserModel.find(
+        get(submittingAuthorData, 'id'),
+      )
+
+      const authorsPromises = authors.map(async author => {
+        const user = await UserModel.find(author.id)
+        return `${user.firstName} ${user.lastName}`
+      })
+      const authorsList = await Promise.all(authorsPromises)
+
+      return {
+        authorsList,
+        submittingAuthor,
+      }
+    } catch (e) {
+      throw e
+    }
+  }
+
+  getReviewerInvitations({ agree = true }) {
+    const { fragment: { invitations = [] } } = this
+    return agree
+      ? invitations.filter(
+          inv =>
+            inv.role === 'reviewer' &&
+            inv.hasAnswer === true &&
+            inv.isAccepted === true,
+        )
+      : invitations.filter(
+          inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+        )
+  }
+
+  getHeRequestToRevision() {
+    const { fragment: { recommendations = [] } } = this
+    return recommendations.find(
+      rec =>
+        rec.recommendationType === 'editorRecommendation' &&
+        (rec.recommendation === 'minor' || rec.recommendation === 'major'),
+    )
+  }
 }
 
 module.exports = Fragment
diff --git a/packages/component-helper-service/src/services/Invitation.js b/packages/component-helper-service/src/services/Invitation.js
index ac595dabf0ee0f4880a343f29c8c4376c171a1e6..ce06b5ad4d8617a2c97506bc5759cdbb10200906 100644
--- a/packages/component-helper-service/src/services/Invitation.js
+++ b/packages/component-helper-service/src/services/Invitation.js
@@ -33,7 +33,7 @@ class Invitation {
     return { invitedOn, respondedOn, status, id }
   }
 
-  async setupInvitation({ collection }) {
+  async createInvitation({ parentObject }) {
     const { userId, role } = this
     const invitation = {
       role,
@@ -44,9 +44,10 @@ class Invitation {
       userId,
       respondedOn: null,
     }
-    collection.invitations = collection.invitations || []
-    collection.invitations.push(invitation)
-    collection = await collection.save()
+    parentObject.invitations = parentObject.invitations || []
+    parentObject.invitations.push(invitation)
+    await parentObject.save()
+
     return invitation
   }
 
@@ -56,6 +57,22 @@ class Invitation {
         invitation.userId === this.userId && invitation.role === this.role,
     )
   }
+
+  validateInvitation({ invitation }) {
+    if (invitation === undefined)
+      return { status: 404, error: 'Invitation not found.' }
+
+    if (invitation.hasAnswer)
+      return { status: 400, error: 'Invitation has already been answered.' }
+
+    if (invitation.userId !== this.userId)
+      return {
+        status: 403,
+        error: 'User is not allowed to modify this invitation.',
+      }
+
+    return { error: null }
+  }
 }
 
 module.exports = Invitation
diff --git a/packages/component-helper-service/src/services/Team.js b/packages/component-helper-service/src/services/Team.js
index b5b70ee1eb1f4ecb467f985ada697602f1247811..9d8d6148a8159b24180e22164247fcf3874aa2fa 100644
--- a/packages/component-helper-service/src/services/Team.js
+++ b/packages/component-helper-service/src/services/Team.js
@@ -2,13 +2,16 @@ const logger = require('@pubsweet/logger')
 const get = require('lodash/get')
 
 class Team {
-  constructor({ TeamModel = {}, collectionId = '' }) {
+  constructor({ TeamModel = {}, fragmentId = '', collectionId = '' }) {
     this.TeamModel = TeamModel
+    this.fragmentId = fragmentId
     this.collectionId = collectionId
   }
 
-  async createNewTeam({ role, userId }) {
-    const { collectionId, TeamModel } = this
+  async createTeam({ role, userId, objectType }) {
+    const { fragmentId, TeamModel, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
+
     let permissions, group, name
     switch (role) {
       case 'handlingEditor':
@@ -38,8 +41,8 @@ class Team {
       group,
       name,
       object: {
-        type: 'collection',
-        id: collectionId,
+        type: objectType,
+        id: objectId,
       },
       members: [userId],
     }
@@ -48,15 +51,16 @@ class Team {
     return team
   }
 
-  async setupManuscriptTeam({ user, role }) {
-    const { TeamModel, collectionId } = this
+  async setupTeam({ user, role, objectType }) {
+    const { TeamModel, fragmentId, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
     const teams = await TeamModel.all()
     user.teams = user.teams || []
     let foundTeam = teams.find(
       team =>
         team.group === role &&
-        team.object.type === 'collection' &&
-        team.object.id === collectionId,
+        team.object.type === objectType &&
+        team.object.id === objectId,
     )
 
     if (foundTeam !== undefined) {
@@ -75,7 +79,7 @@ class Team {
         logger.error(e)
       }
     } else {
-      const team = await this.createNewTeam({ role, userId: user.id })
+      const team = await this.createTeam({ role, userId: user.id, objectType })
       user.teams.push(team.id)
       await user.save()
       return team
@@ -90,16 +94,18 @@ class Team {
     await team.save()
   }
 
-  async getTeamMembersByCollection({ role }) {
-    const { TeamModel, collectionId } = this
+  async getTeamMembers({ role, objectType }) {
+    const { TeamModel, collectionId, fragmentId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
 
     const teams = await TeamModel.all()
+
     const members = get(
       teams.find(
         team =>
           team.group === role &&
-          team.object.type === 'collection' &&
-          team.object.id === collectionId,
+          team.object.type === objectType &&
+          team.object.id === objectId,
       ),
       'members',
     )
@@ -107,20 +113,22 @@ class Team {
     return members
   }
 
-  async getTeamByGroupAndCollection({ role }) {
-    const { TeamModel, collectionId } = this
+  async getTeam({ role, objectType }) {
+    const { TeamModel, fragmentId, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
     const teams = await TeamModel.all()
     return teams.find(
       team =>
         team.group === role &&
-        team.object.type === 'collection' &&
-        team.object.id === collectionId,
+        team.object.type === objectType &&
+        team.object.id === objectId,
     )
   }
 
-  async updateHETeam({ collection, role, user }) {
-    const team = await this.getTeamByGroupAndCollection({
+  async deleteHandlingEditor({ collection, role, user }) {
+    const team = await this.getTeam({
       role,
+      objectType: 'collection',
     })
     delete collection.handlingEditor
     await this.removeTeamMember({ teamId: team.id, userId: user.id })
diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js
index ed69cd655e6acd1445dacab855d2ddbde74dd7b9..7d9d3e520951e13f9bc1e2aeeac2cbf7e5528c00 100644
--- a/packages/component-helper-service/src/services/User.js
+++ b/packages/component-helper-service/src/services/User.js
@@ -31,12 +31,8 @@ class User {
 
     let newUser = new UserModel(userBody)
 
-    try {
-      newUser = await newUser.save()
-      return newUser
-    } catch (e) {
-      logger.error(e)
-    }
+    newUser = await newUser.save()
+    return newUser
   }
 
   async setupNewUser({ url, role, invitationType, body = {} }) {
diff --git a/packages/component-helper-service/src/services/authsome.js b/packages/component-helper-service/src/services/authsome.js
index 212cee2a3ea23a424b1f77dde2f7dd4cf888b2a0..b2215bb20f4d5d50cd0f75fff2607ba79f514443 100644
--- a/packages/component-helper-service/src/services/authsome.js
+++ b/packages/component-helper-service/src/services/authsome.js
@@ -12,6 +12,7 @@ const getAuthsome = models =>
       models: {
         Collection: {
           find: id => models.Collection.find(id),
+          all: () => models.Collection.all(),
         },
         Fragment: {
           find: id => models.Fragment.find(id),
diff --git a/packages/component-invite/config/authsome-helpers.js b/packages/component-invite/config/authsome-helpers.js
index b5f42492b077cee12d5a09d97404fb559e601f5e..55148df349bc9807c0d6121fe30593dd021d237b 100644
--- a/packages/component-invite/config/authsome-helpers.js
+++ b/packages/component-invite/config/authsome-helpers.js
@@ -14,9 +14,9 @@ const parseAuthorsData = (coll, matchingCollPerm) => {
 
 const setPublicStatuses = (coll, matchingCollPerm) => {
   const status = get(coll, 'status') || 'draft'
-  coll.visibleStatus = statuses[status].public
-  if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) {
-    coll.visibleStatus = statuses[coll.status].private
+  // coll.visibleStatus = statuses[status].public
+  if (publicStatusesPermissions.includes(matchingCollPerm.permission)) {
+    coll.visibleStatus = statuses[status].public
   }
 }
 
@@ -44,7 +44,10 @@ const filterObjectData = (
           rec => rec.userId === user.id,
         )
     }
-
+    parseAuthorsData(object, matchingCollPerm)
+    if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
+      return filterRefusedInvitations(object, user)
+    }
     return object
   }
   const matchingCollPerm = collectionsPermissions.find(
@@ -52,32 +55,77 @@ const filterObjectData = (
   )
   if (matchingCollPerm === undefined) return null
   setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
 
   return object
 }
 
-const getTeamsByPermissions = async (teamIds, permissions, TeamModel) => {
-  const teams = await Promise.all(
+const getTeamsByPermissions = async (
+  teamIds = [],
+  permissions = [],
+  TeamModel,
+) =>
+  (await Promise.all(
     teamIds.map(async teamId => {
       const team = await TeamModel.find(teamId)
-      if (permissions.includes(team.teamType.permissions)) {
-        return team
+      if (!permissions.includes(team.teamType.permissions)) {
+        return null
       }
-      return null
+      return team
     }),
+  )).filter(Boolean)
+
+const heIsInvitedToFragment = async ({ user, Team, collectionId }) =>
+  (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some(
+    // user is a member of the team with access to the fragment's parent collection
+    t => t.members.includes(user.id) && t.object.id === collectionId,
   )
 
-  return teams.filter(Boolean)
+const getUserPermissions = async ({
+  user,
+  Team,
+  mapFn = t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }),
+}) =>
+  (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn)
+
+const isOwner = ({ user: { id }, object }) => {
+  if (object.owners.includes(id)) return true
+  return !!object.owners.find(own => own.id === id)
+}
+
+const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => {
+  const userPermissions = await getUserPermissions({
+    user,
+    Team,
+  })
+
+  return !!userPermissions.find(p => {
+    const hasObject =
+      p.objectId === get(object, 'fragment.id') ||
+      p.objectId === get(object, 'fragment.collectionId')
+    if (roles.length > 0) {
+      return hasObject && roles.includes(p.role)
+    }
+    return hasObject
+  })
 }
 
+const isHandlingEditor = ({ user, object }) =>
+  get(object, 'collection.handlingEditor.id') === user.id
+
 module.exports = {
+  filterObjectData,
   parseAuthorsData,
   setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
   getTeamsByPermissions,
+  filterRefusedInvitations,
+  //
+  isOwner,
+  isHandlingEditor,
+  getUserPermissions,
+  heIsInvitedToFragment,
+  hasPermissionForObject,
 }
diff --git a/packages/component-invite/config/authsome-mode.js b/packages/component-invite/config/authsome-mode.js
index 762998f83e80d5e678595bb8bf7e57072607adbe..8a92a1301df7ea1bed2dc105beb44834cdb9b16f 100644
--- a/packages/component-invite/config/authsome-mode.js
+++ b/packages/component-invite/config/authsome-mode.js
@@ -4,6 +4,7 @@ const omit = require('lodash/omit')
 const helpers = require('./authsome-helpers')
 
 async function teamPermissions(user, operation, object, context) {
+  const { models } = context
   const permissions = ['handlingEditor', 'author', 'reviewer']
   const teams = await helpers.getTeamsByPermissions(
     user.teams,
@@ -11,22 +12,38 @@ async function teamPermissions(user, operation, object, context) {
     context.models.Team,
   )
 
-  const collectionsPermissions = await Promise.all(
+  let collectionsPermissions = await Promise.all(
     teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
+      let collection
+      if (team.object.type === 'collection') {
+        collection = await models.Collection.find(team.object.id)
+      } else if (team.object.type === 'fragment') {
+        const fragment = await models.Fragment.find(team.object.id)
+        collection = await models.Collection.find(fragment.collectionId)
+      }
+      if (
+        collection.status === 'rejected' &&
+        team.teamType.permissions === 'reviewer'
+      )
+        return null
       const collPerm = {
         id: collection.id,
         permission: team.teamType.permissions,
       }
       const objectType = get(object, 'type')
-      if (objectType === 'fragment' && collection.fragments.includes(object.id))
-        collPerm.fragmentId = object.id
+      if (objectType === 'fragment') {
+        if (collection.fragments.includes(object.id))
+          collPerm.fragmentId = object.id
+        else return null
+      }
 
+      if (objectType === 'collection')
+        if (object.id !== collection.id) return null
       return collPerm
     }),
   )
-
-  if (collectionsPermissions.length === 0) return {}
+  collectionsPermissions = collectionsPermissions.filter(cp => cp !== null)
+  if (collectionsPermissions.length === 0) return false
 
   return {
     filter: filterParam => {
@@ -109,11 +126,16 @@ async function authenticatedUser(user, operation, object, context) {
     return true
   }
 
-  // Allow the authenticated user to GET collections they own
-  if (operation === 'GET' && object === '/collections/') {
-    return {
-      filter: collection => collection.owners.includes(user.id),
+  // allow authenticate owners full pass for a collection
+  if (get(object, 'type') === 'collection') {
+    if (operation === 'PATCH') {
+      return {
+        filter: collection => omit(collection, 'filtered'),
+      }
     }
+    if (object.owners.includes(user.id)) return true
+    const owner = object.owners.find(own => own.id === user.id)
+    if (owner !== undefined) return true
   }
 
   // Allow owners of a collection to GET its teams, e.g.
@@ -170,32 +192,36 @@ async function authenticatedUser(user, operation, object, context) {
     return false
   }
 
-  // only allow a reviewer to submit and to modify a recommendation
+  // only allow a reviewer and an HE to submit and to modify a recommendation
   if (
     ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
     object.path.includes('recommendations')
   ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
+    const authsomeObject = get(object, 'authsomeObject')
+
     const teams = await helpers.getTeamsByPermissions(
       user.teams,
-      ['reviewer'],
+      ['reviewer', 'handlingEditor'],
       context.models.Team,
     )
+
     if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
+    const matchingTeam = teams.find(
+      team => team.object.id === authsomeObject.id,
+    )
+
     if (matchingTeam) return true
     return false
   }
 
-  if (user.teams.length !== 0 && operation === 'GET') {
+  if (user.teams.length !== 0 && ['GET'].includes(operation)) {
     const permissions = await teamPermissions(user, operation, object, context)
 
     if (permissions) {
       return permissions
     }
+
+    return false
   }
 
   if (get(object, 'type') === 'fragment') {
@@ -206,19 +232,6 @@ async function authenticatedUser(user, operation, object, context) {
     }
   }
 
-  if (get(object, 'type') === 'collection') {
-    if (['GET', 'DELETE'].includes(operation)) {
-      return true
-    }
-
-    // Only allow filtered updating (mirroring filtered creation) for non-admin users)
-    if (operation === 'PATCH') {
-      return {
-        filter: collection => omit(collection, 'filtered'),
-      }
-    }
-  }
-
   // A user can GET, DELETE and PATCH itself
   if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
     if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
@@ -240,7 +253,7 @@ const authsomeMode = async (userId, operation, object, context) => {
   const user = await context.models.User.find(userId)
 
   // Admins and editor in chiefs can do anything
-  if (user && (user.admin === true || user.editorInChief === true)) return true
+  if (user && (user.admin || user.editorInChief)) return true
 
   if (user) {
     return authenticatedUser(user, operation, object, context)
diff --git a/packages/component-invite/index.js b/packages/component-invite/index.js
index 0e21fe5223f0a0a71bebd53d509b0bef3a5e3e02..f9bc6f5a6ef371301ad40dc80c6f06c59b67577f 100644
--- a/packages/component-invite/index.js
+++ b/packages/component-invite/index.js
@@ -1,5 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/CollectionsInvitations')(app)
+    require('./src/FragmentsInvitations')(app)
   },
 }
diff --git a/packages/component-invite/src/CollectionsInvitations.js b/packages/component-invite/src/CollectionsInvitations.js
index ba6fa2d852bf1a14b39b182297eff50f2c8661b7..2f80dd1b70e3089a7ab7f9068a4e12dbb84985e5 100644
--- a/packages/component-invite/src/CollectionsInvitations.js
+++ b/packages/component-invite/src/CollectionsInvitations.js
@@ -14,13 +14,13 @@ const CollectionsInvitations = app => {
    * @apiParamExample {json} Body
    *    {
    *      "email": "email@example.com",
-   *      "role": "handlingEditor", [acceptedValues: handlingEditor, reviewer]
+   *      "role": "handlingEditor", [acceptedValues: handlingEditor]
    *    }
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 OK
    *   {
    *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
-   *     "role": "reviewer",
+   *     "role": "handlingEditor",
    *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
    *     "hasAnswer": false,
    *     "invitedOn": 1525428890167,
@@ -38,32 +38,6 @@ const CollectionsInvitations = app => {
     authBearer,
     require(`${routePath}/post`)(app.locals.models),
   )
-  /**
-   * @api {get} /api/collections/:collectionId/invitations/[:invitationId]?role=:role List collections invitations
-   * @apiGroup CollectionsInvitations
-   * @apiParam {id} collectionId Collection id
-   * @apiParam {id} [invitationId] Invitation id
-   * @apiParam {String} role The role to search for: handlingEditor, reviewer, author
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    [{
-   *      "name": "John Smith",
-   *     "invitedOn": 1525428890167,
-   *     "respondedOn": 1525428890299,
-   *      "email": "email@example.com",
-   *      "status": "pending",
-   *      "invitationId": "1990881"
-   *    }]
-   * @apiErrorExample {json} List errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   */
-  app.get(
-    `${basePath}/:invitationId?`,
-    authBearer,
-    require(`${routePath}/get`)(app.locals.models),
-  )
   /**
    * @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation
    * @apiGroup CollectionsInvitations
@@ -95,7 +69,7 @@ const CollectionsInvitations = app => {
    *    HTTP/1.1 200 OK
    *   {
    *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
-   *     "role": "reviewer",
+   *     "role": "handlingEditor",
    *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
    *     "hasAnswer": true,
    *     "invitedOn": 1525428890167,
@@ -113,28 +87,6 @@ const CollectionsInvitations = app => {
     authBearer,
     require(`${routePath}/patch`)(app.locals.models),
   )
-  /**
-   * @api {patch} /api/collections/:collectionId/invitations/:invitationId/decline Decline an invitation as a reviewer
-   * @apiGroup CollectionsInvitations
-   * @apiParam {collectionId} collectionId Collection id
-   * @apiParam {invitationId} invitationId Invitation id
-   * @apiParamExample {json} Body
-   *    {
-   *      "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f",
-   *    }
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    {}
-   * @apiErrorExample {json} Update invitations errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   *    HTTP/1.1 500 Internal Server Error
-   */
-  app.patch(
-    `${basePath}/:invitationId/decline`,
-    require(`${routePath}/decline`)(app.locals.models),
-  )
 }
 
 module.exports = CollectionsInvitations
diff --git a/packages/component-invite/src/FragmentsInvitations.js b/packages/component-invite/src/FragmentsInvitations.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a4d2507424cbeb5a5a0c0357779508fed009a8d
--- /dev/null
+++ b/packages/component-invite/src/FragmentsInvitations.js
@@ -0,0 +1,145 @@
+const bodyParser = require('body-parser')
+
+const FragmentsInvitations = app => {
+  app.use(bodyParser.json())
+  const basePath =
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+  const routePath = './routes/fragmentsInvitations'
+  const authBearer = app.locals.passport.authenticate('bearer', {
+    session: false,
+  })
+  /**
+   * @api {post} /api/collections/:collectionId/fragments/:fragmentId/invitations Invite a user to a fragment
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParamExample {json} Body
+   *    {
+   *      "email": "email@example.com",
+   *      "role": "reviewer", [acceptedValues: reviewer]
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *   {
+   *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
+   *     "role": "reviewer",
+   *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
+   *     "hasAnswer": false,
+   *     "invitedOn": 1525428890167,
+   *     "isAccepted": false,
+   *     "respondedOn": null
+   *    }
+   * @apiErrorExample {json} Invite user errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.post(
+    basePath,
+    authBearer,
+    require(`${routePath}/post`)(app.locals.models),
+  )
+  /**
+   * @api {get} /api/collections/:collectionId/fragments/:fragmentId/invitations/[:invitationId]?role=:role List fragment invitations
+   * @apiGroup FragmentsInvitations
+   * @apiParam {id} collectionId Collection id
+   * @apiParam {id} fragmentId Fragment id
+   * @apiParam {id} [invitationId] Invitation id
+   * @apiParam {String} role The role to search for: reviewer
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    [{
+   *      "name": "John Smith",
+   *     "invitedOn": 1525428890167,
+   *     "respondedOn": 1525428890299,
+   *      "email": "email@example.com",
+   *      "status": "pending",
+   *      "invitationId": "1990881"
+   *    }]
+   * @apiErrorExample {json} List errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   */
+  app.get(
+    `${basePath}/:invitationId?`,
+    authBearer,
+    require(`${routePath}/get`)(app.locals.models),
+  )
+  /**
+   * @api {delete} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Delete invitation
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 204 No Content
+   * @apiErrorExample {json} Delete errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.delete(
+    `${basePath}/:invitationId`,
+    authBearer,
+    require(`${routePath}/delete`)(app.locals.models),
+  )
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Update an invitation
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParamExample {json} Body
+   *    {
+   *      "isAccepted": false,
+   *      "reason": "I am not ready" [optional]
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *   {
+   *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
+   *     "role": "reviewer",
+   *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
+   *     "hasAnswer": true,
+   *     "invitedOn": 1525428890167,
+   *     "isAccepted": false,
+   *     "respondedOn": 1525428890299
+   *    }
+   * @apiErrorExample {json} Update invitations errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}/:invitationId`,
+    authBearer,
+    require(`${routePath}/patch`)(app.locals.models),
+  )
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId/decline Decline an invitation as a reviewer
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiParamExample {json} Body
+   *    {
+   *      "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f",
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    {}
+   * @apiErrorExample {json} Update invitations errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}/:invitationId/decline`,
+    require(`${routePath}/decline`)(app.locals.models),
+  )
+}
+
+module.exports = FragmentsInvitations
diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js
index e3e69c0ecc26e772b0428004df9fa9134010d141..b7c77e4f41780107d756bb3637ed55b45ac0eff9 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/delete.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js
@@ -1,14 +1,9 @@
 const config = require('config')
-const logger = require('@pubsweet/logger')
-const last = require('lodash/last')
 const mailService = require('pubsweet-component-mail-service')
 
 const {
   services,
   Team,
-  Email,
-  Fragment,
-  Collection,
   authsome: authsomeHelper,
 } = require('pubsweet-component-helper-service')
 
@@ -19,7 +14,7 @@ module.exports = models => async (req, res) => {
 
   try {
     const collection = await models.Collection.find(collectionId)
-    const collectionHelper = new Collection({ collection })
+
     const authsome = authsomeHelper.getAuthsome(models)
     const target = {
       collection,
@@ -30,79 +25,48 @@ module.exports = models => async (req, res) => {
       return res.status(403).json({
         error: 'Unauthorized.',
       })
-    const invitation = await collection.invitations.find(
+
+    collection.invitations = collection.invitations || []
+    const invitation = collection.invitations.find(
       invitation => invitation.id === invitationId,
     )
-    if (invitation === undefined) {
-      res.status(404).json({
+    if (!invitation)
+      return res.status(404).json({
         error: `Invitation ${invitationId} not found`,
       })
-      return
-    }
 
-    const team = await teamHelper.getTeamByGroupAndCollection({
+    const team = await teamHelper.getTeam({
       role: invitation.role,
+      objectType: 'collection',
     })
 
     collection.invitations = collection.invitations.filter(
       inv => inv.id !== invitation.id,
     )
-    if (invitation.role === 'handlingEditor') {
-      collection.status = 'submitted'
-      collection.visibleStatus = statuses[collection.status].private
-      delete collection.handlingEditor
-    } else if (invitation.role === 'reviewer') {
-      await collectionHelper.updateStatusByNumberOfReviewers()
-    }
+
+    collection.status = 'submitted'
+    collection.visibleStatus = statuses[collection.status].private
+    delete collection.handlingEditor
     await collection.save()
+
     await teamHelper.removeTeamMember({
       teamId: team.id,
       userId: invitation.userId,
     })
+
     const UserModel = models.User
     const user = await UserModel.find(invitation.userId)
     user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
     await user.save()
-    try {
-      if (invitation.role === 'handlingEditor') {
-        await mailService.sendSimpleEmail({
-          toEmail: user.email,
-          emailType: 'revoke-handling-editor',
-        })
-      } else if (invitation.role === 'reviewer') {
-        const collectionHelper = new Collection({ collection })
-        const fragment = await models.Fragment.find(last(collection.fragments))
-        const fragmentHelper = new Fragment({ fragment })
-        const parsedFragment = await fragmentHelper.getFragmentData({
-          handlingEditor: collection.handlingEditor,
-        })
-        const baseUrl = services.getBaseUrl(req)
-        const {
-          authorsList: authors,
-          submittingAuthor,
-        } = await collectionHelper.getAuthorData({ UserModel })
-        const emailHelper = new Email({
-          UserModel,
-          collection,
-          parsedFragment,
-          baseUrl,
-          authors,
-        })
-        emailHelper.setupReviewerUnassignEmail({
-          user,
-          authorName: `${submittingAuthor.firstName} ${
-            submittingAuthor.lastName
-          }`,
-        })
-      }
 
-      return res.status(200).json({})
-    } catch (e) {
-      logger.error(e.message)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
+    mailService.sendSimpleEmail({
+      toEmail: user.email,
+      emailType: 'revoke-handling-editor',
+    })
+
+    return res.status(200).json({})
   } catch (e) {
-    const notFoundError = await services.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Collection')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js
index 69d6302f6184d69ead39341d5867748bec23477d..d28609053609980f7256195dda6e4c98bdc9f78e 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/patch.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js
@@ -1,144 +1,89 @@
-const logger = require('@pubsweet/logger')
 const mailService = require('pubsweet-component-mail-service')
-const last = require('lodash/last')
 
 const {
-  Email,
-  services,
-  Fragment,
-  Collection,
   Team,
   User,
+  services,
+  Collection,
+  Invitation,
 } = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { collectionId, invitationId } = req.params
   const { isAccepted, reason } = req.body
 
-  if (!services.checkForUndefinedParams(isAccepted)) {
-    res.status(400).json({ error: 'Missing parameters.' })
-    logger.error('some parameters are missing')
-    return
-  }
   const UserModel = models.User
   const user = await UserModel.find(req.user)
+
   try {
     const collection = await models.Collection.find(collectionId)
-    const invitation = await collection.invitations.find(
+    collection.invitations = collection.invitations || []
+    const invitation = collection.invitations.find(
       invitation => invitation.id === invitationId,
     )
-    if (invitation === undefined)
-      return res.status(404).json({
-        error: 'Invitation not found.',
-      })
-    if (invitation.hasAnswer)
-      return res
-        .status(400)
-        .json({ error: 'Invitation has already been answered.' })
-    if (invitation.userId !== user.id)
-      return res.status(403).json({
-        error: `User is not allowed to modify this invitation.`,
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'handlingEditor',
+    })
+
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
       })
 
     const collectionHelper = new Collection({ collection })
-    const fragment = await models.Fragment.find(last(collection.fragments))
-    const fragmentHelper = new Fragment({ fragment })
-    const parsedFragment = await fragmentHelper.getFragmentData({
-      handlingEditor: collection.handlingEditor,
-    })
     const baseUrl = services.getBaseUrl(req)
-    const {
-      authorsList: authors,
-      submittingAuthor,
-    } = await collectionHelper.getAuthorData({ UserModel })
-    const emailHelper = new Email({
-      UserModel,
-      collection,
-      parsedFragment,
-      baseUrl,
-      authors,
-    })
+
     const teamHelper = new Team({ TeamModel: models.Team, collectionId })
     const userHelper = new User({ UserModel })
 
-    if (invitation.role === 'handlingEditor')
-      await collectionHelper.updateHandlingEditor({ isAccepted })
+    await collectionHelper.updateHandlingEditor({ isAccepted })
     invitation.respondedOn = Date.now()
     invitation.hasAnswer = true
     const eic = await userHelper.getEditorInChief()
     const toEmail = eic.email
-    if (isAccepted === true) {
-      invitation.isAccepted = true
-      if (
-        invitation.role === 'reviewer' &&
-        collection.status === 'reviewersInvited'
-      )
-        await collectionHelper.updateStatus({ newStatus: 'underReview' })
 
-      await collection.save()
-      try {
-        if (invitation.role === 'handlingEditor')
-          await mailService.sendSimpleEmail({
-            toEmail,
-            user,
-            emailType: 'handling-editor-agreed',
-            dashboardUrl: baseUrl,
-            meta: {
-              collectionId: collection.customId,
-            },
-          })
-        if (invitation.role === 'reviewer')
-          emailHelper.setupReviewerDecisionEmail({
-            agree: true,
-            timestamp: invitation.respondedOn,
-            user,
-            authorName: `${submittingAuthor.firstName} ${
-              submittingAuthor.lastName
-            }`,
-          })
-        return res.status(200).json(invitation)
-      } catch (e) {
-        logger.error(e)
-        return res.status(500).json({ error: 'Email could not be sent.' })
-      }
-    } else {
-      invitation.isAccepted = false
-
-      if (invitation.role === 'handlingEditor')
-        await teamHelper.updateHETeam({
-          collection,
-          role: invitation.role,
-          user,
-        })
-      if (reason !== undefined) {
-        invitation.reason = reason
-      }
+    if (isAccepted) {
+      invitation.isAccepted = true
       await collection.save()
 
-      try {
-        if (invitation.role === 'handlingEditor') {
-          await mailService.sendSimpleEmail({
-            toEmail,
-            user,
-            emailType: 'handling-editor-declined',
-            meta: {
-              reason,
-              collectionId: collection.customId,
-            },
-          })
-        } else if (invitation.role === 'reviewer') {
-          collectionHelper.updateStatusByNumberOfReviewers()
-          emailHelper.setupReviewerDecisionEmail({
-            agree: false,
-            user,
-          })
-        }
-      } catch (e) {
-        logger.error(e)
-        return res.status(500).json({ error: 'Email could not be sent.' })
-      }
+      mailService.sendSimpleEmail({
+        toEmail,
+        user,
+        emailType: 'handling-editor-agreed',
+        dashboardUrl: baseUrl,
+        meta: {
+          collectionId: collection.customId,
+        },
+      })
+
+      return res.status(200).json(invitation)
     }
-    user.save()
+
+    await teamHelper.deleteHandlingEditor({
+      collection,
+      role: invitation.role,
+      user,
+    })
+
+    invitation.isAccepted = false
+    if (reason) invitation.reason = reason
+    await collection.save()
+
+    mailService.sendSimpleEmail({
+      toEmail,
+      user,
+      emailType: 'handling-editor-declined',
+      meta: {
+        reason,
+        collectionId: collection.customId,
+      },
+    })
+
     return res.status(200).json(invitation)
   } catch (e) {
     const notFoundError = await services.handleNotFoundError(e, 'collection')
diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js
index 3bafaf954c6276637c2195b93ff0651ab53cdb05..3e20a3c89450bce8077a350249b150ac975dcab4 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/post.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/post.js
@@ -1,49 +1,38 @@
-const config = require('config')
-const get = require('lodash/get')
-const last = require('lodash/last')
 const logger = require('@pubsweet/logger')
 const mailService = require('pubsweet-component-mail-service')
 const {
-  Email,
   services,
   authsome: authsomeHelper,
-  Fragment,
   Collection,
   Team,
   Invitation,
-  User,
 } = require('pubsweet-component-helper-service')
 
-const configRoles = config.get('roles')
-
 module.exports = models => async (req, res) => {
   const { email, role } = req.body
 
-  if (!services.checkForUndefinedParams(email, role)) {
-    res.status(400).json({ error: 'Email and role are required' })
-    logger.error('User ID and role are missing')
-    return
-  }
+  if (!services.checkForUndefinedParams(email, role))
+    return res.status(400).json({ error: 'Email and role are required' })
+
+  if (role !== 'handlingEditor')
+    return res.status(400).json({
+      error: `Role ${role} is invalid. Only handlingEditor is allowed.`,
+    })
 
-  if (!configRoles.collection.includes(role)) {
-    res.status(400).json({ error: `Role ${role} is invalid` })
-    logger.error(`invitation attempted on invalid role ${role}`)
-    return
-  }
   const UserModel = models.User
   const reqUser = await UserModel.find(req.user)
 
   if (email === reqUser.email && !reqUser.admin) {
     logger.error(`${reqUser.email} tried to invite his own email`)
-    return res.status(400).json({ error: 'Cannot invite yourself' })
+    return res.status(400).json({ error: 'Cannot invite yourself.' })
   }
 
-  const collectionId = get(req, 'params.collectionId')
+  const { collectionId } = req.params
   let collection
   try {
     collection = await models.Collection.find(collectionId)
   } catch (e) {
-    const notFoundError = await services.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Collection')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
@@ -61,115 +50,49 @@ module.exports = models => async (req, res) => {
     })
 
   const collectionHelper = new Collection({ collection })
-  const fragment = await models.Fragment.find(last(collection.fragments))
-  const fragmentHelper = new Fragment({ fragment })
-  const handlingEditor = collection.handlingEditor || {}
-  const parsedFragment = await fragmentHelper.getFragmentData({
-    handlingEditor,
-  })
   const baseUrl = services.getBaseUrl(req)
-  const {
-    authorsList: authors,
-    submittingAuthor,
-  } = await collectionHelper.getAuthorData({ UserModel })
-  const emailHelper = new Email({
-    UserModel,
-    collection,
-    parsedFragment,
-    baseUrl,
-    authors,
+
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
   })
-  const teamHelper = new Team({ TeamModel: models.Team, collectionId })
   const invitationHelper = new Invitation({ role })
 
   try {
     const user = await UserModel.findByEmail(email)
-    await teamHelper.setupManuscriptTeam({ user, role })
+    await teamHelper.setupTeam({ user, role, objectType: 'collection' })
     invitationHelper.userId = user.id
     let invitation = invitationHelper.getInvitation({
       invitations: collection.invitations,
     })
 
-    let resend = false
-    if (invitation !== undefined) {
+    if (invitation) {
       if (invitation.hasAnswer)
         return res
           .status(400)
           .json({ error: 'User has already replied to a previous invitation.' })
       invitation.invitedOn = Date.now()
       await collection.save()
-      resend = true
     } else {
-      invitation = await invitationHelper.setupInvitation({
-        collection,
+      invitation = await invitationHelper.createInvitation({
+        parentObject: collection,
       })
     }
 
-    try {
-      if (role === 'reviewer') {
-        if (collection.status === 'heAssigned')
-          await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
+    invitation.invitedOn = Date.now()
+    await collection.save()
+    await collectionHelper.addHandlingEditor({ user, invitation })
 
-        await emailHelper.setupReviewerInvitationEmail({
-          user,
-          invitationId: invitation.id,
-          timestamp: invitation.invitedOn,
-          resend,
-          authorName: `${submittingAuthor.firstName} ${
-            submittingAuthor.lastName
-          }`,
-        })
-      }
+    mailService.sendSimpleEmail({
+      toEmail: user.email,
+      user,
+      emailType: 'assign-handling-editor',
+      dashboardUrl: baseUrl,
+    })
 
-      if (role === 'handlingEditor') {
-        invitation.invitedOn = Date.now()
-        await collection.save()
-        await collectionHelper.addHandlingEditor({ user, invitation })
-        mailService.sendSimpleEmail({
-          toEmail: user.email,
-          user,
-          emailType: 'assign-handling-editor',
-          dashboardUrl: baseUrl,
-        })
-      }
-      return res.status(200).json(invitation)
-    } catch (e) {
-      logger.error(e)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
+    return res.status(200).json(invitation)
   } catch (e) {
-    const userHelper = new User({ UserModel })
-    if (role === 'reviewer') {
-      const newUser = await userHelper.setupNewUser({
-        url: baseUrl,
-        role,
-        invitationType: 'invite',
-        body: req.body,
-      })
-      if (newUser.error !== undefined) {
-        return res.status(newUser.status).json({
-          error: newUser.message,
-        })
-      }
-      if (collection.status === 'heAssigned')
-        await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
-      await teamHelper.setupManuscriptTeam({ user: newUser, role })
-      invitationHelper.userId = newUser.id
-      const invitation = await invitationHelper.setupInvitation({
-        collection,
-      })
-
-      await emailHelper.setupReviewerInvitationEmail({
-        user: newUser,
-        invitationId: invitation.id,
-        timestamp: invitation.invitedOn,
-        authorName: `${submittingAuthor.firstName} ${
-          submittingAuthor.lastName
-        }`,
-      })
-      return res.status(200).json(invitation)
-    }
-    const notFoundError = await services.handleNotFoundError(e, 'user')
+    const notFoundError = await services.handleNotFoundError(e, 'User')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/collectionsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js
similarity index 64%
rename from packages/component-invite/src/routes/collectionsInvitations/decline.js
rename to packages/component-invite/src/routes/fragmentsInvitations/decline.js
index b049a4a0aeb43f096b0ec673b338d91202440cc4..e867503771902281e8686ff0330e2508ca693dfa 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/decline.js
+++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js
@@ -1,14 +1,12 @@
-const last = require('lodash/last')
-
 const {
   services,
   Email,
   Fragment,
-  Collection,
+  Invitation,
 } = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
-  const { collectionId, invitationId } = req.params
+  const { collectionId, invitationId, fragmentId } = req.params
   const { invitationToken } = req.body
 
   if (!services.checkForUndefinedParams(invitationToken))
@@ -21,30 +19,34 @@ module.exports = models => async (req, res) => {
       invitationToken,
     )
     const collection = await models.Collection.find(collectionId)
-    const invitation = await collection.invitations.find(
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+    fragment.invitations = fragment.invitations || []
+    const invitation = await fragment.invitations.find(
       invitation => invitation.id === invitationId,
     )
-    if (invitation === undefined)
-      return res.status(404).json({
-        error: `Invitation ${invitationId} not found`,
-      })
-    if (invitation.hasAnswer)
-      return res
-        .status(400)
-        .json({ error: `Invitation has already been answered.` })
-    if (invitation.userId !== user.id)
-      return res.status(403).json({
-        error: `User ${user.email} is not allowed to modify invitation ${
-          invitation.id
-        }`,
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'reviewer',
+    })
+
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
       })
 
     invitation.respondedOn = Date.now()
     invitation.hasAnswer = true
     invitation.isAccepted = false
-    await collection.save()
-    const collectionHelper = new Collection({ collection })
-    const fragment = await models.Fragment.find(last(collection.fragments))
+    await fragment.save()
+
     const fragmentHelper = new Fragment({ fragment })
     const parsedFragment = await fragmentHelper.getFragmentData({
       handlingEditor: collection.handlingEditor,
@@ -53,7 +55,7 @@ module.exports = models => async (req, res) => {
     const {
       authorsList: authors,
       submittingAuthor,
-    } = await collectionHelper.getAuthorData({ UserModel })
+    } = await fragmentHelper.getAuthorData({ UserModel })
     const emailHelper = new Email({
       UserModel,
       collection,
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/delete.js b/packages/component-invite/src/routes/fragmentsInvitations/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4da00bc268d61d124f9ef248f4ad3af04f91ecd
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/delete.js
@@ -0,0 +1,97 @@
+const {
+  services,
+  Team,
+  Email,
+  Fragment,
+  Collection,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, invitationId, fragmentId } = req.params
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+    fragmentId,
+  })
+
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+
+    const collectionHelper = new Collection({ collection })
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      collection,
+      path: req.route.path,
+    }
+    const canDelete = await authsome.can(req.user, 'DELETE', target)
+    if (!canDelete)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+    fragment.invitations = fragment.invitations || []
+    const invitation = await fragment.invitations.find(
+      invitation => invitation.id === invitationId,
+    )
+    if (!invitation)
+      return res.status(404).json({
+        error: `Invitation ${invitationId} not found`,
+      })
+
+    const team = await teamHelper.getTeam({
+      role: invitation.role,
+      objectType: 'fragment',
+    })
+
+    fragment.invitations = fragment.invitations.filter(
+      inv => inv.id !== invitation.id,
+    )
+
+    await collectionHelper.updateStatusByNumberOfReviewers()
+
+    await teamHelper.removeTeamMember({
+      teamId: team.id,
+      userId: invitation.userId,
+    })
+
+    const UserModel = models.User
+    const user = await UserModel.find(invitation.userId)
+    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
+    await user.save()
+
+    const fragmentHelper = new Fragment({ fragment })
+    const parsedFragment = await fragmentHelper.getFragmentData({
+      handlingEditor: collection.handlingEditor,
+    })
+    const baseUrl = services.getBaseUrl(req)
+    const {
+      authorsList: authors,
+      submittingAuthor,
+    } = await fragmentHelper.getAuthorData({ UserModel })
+    const emailHelper = new Email({
+      UserModel,
+      collection,
+      parsedFragment,
+      baseUrl,
+      authors,
+    })
+
+    emailHelper.setupReviewerUnassignEmail({
+      user,
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+    })
+
+    return res.status(200).json({})
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'collection')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/fragmentsInvitations/get.js
similarity index 70%
rename from packages/component-invite/src/routes/collectionsInvitations/get.js
rename to packages/component-invite/src/routes/fragmentsInvitations/get.js
index 18e1e0eb7a8a8c08a0fb5f3c34e220c91c1678c7..2bb8c540e6576951ba8773108036dda02e8f2fee 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/get.js
+++ b/packages/component-invite/src/routes/fragmentsInvitations/get.js
@@ -20,14 +20,24 @@ module.exports = models => async (req, res) => {
     return
   }
 
-  const { collectionId } = req.params
-  const teamHelper = new Team({ TeamModel: models.Team, collectionId })
+  const { collectionId, fragmentId } = req.params
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+    fragmentId,
+  })
 
   try {
     const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+
     const authsome = authsomeHelper.getAuthsome(models)
     const target = {
-      collection,
+      fragment,
       path: req.route.path,
     }
     const canGet = await authsome.can(req.user, 'GET', target)
@@ -37,23 +47,26 @@ module.exports = models => async (req, res) => {
         error: 'Unauthorized.',
       })
 
-    const members = await teamHelper.getTeamMembersByCollection({
+    const members = await teamHelper.getTeamMembers({
       role,
+      objectType: 'fragment',
     })
-    if (members === undefined) return res.status(200).json([])
+
+    if (!members) return res.status(200).json([])
 
     // TO DO: handle case for when the invitationID is provided
+    const invitationHelper = new Invitation({ role })
+
     const membersData = members.map(async member => {
       const user = await models.User.find(member)
-      const invitationHelper = new Invitation({ userId: user.id, role })
-
+      invitationHelper.userId = user.id
       const {
         invitedOn,
         respondedOn,
         status,
         id,
       } = invitationHelper.getInvitationsData({
-        invitations: collection.invitations,
+        invitations: fragment.invitations,
       })
 
       return {
@@ -70,7 +83,7 @@ module.exports = models => async (req, res) => {
     const resBody = await Promise.all(membersData)
     res.status(200).json(resBody)
   } catch (e) {
-    const notFoundError = await services.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ecb11d6b286fd76b69d892e5222c71d424296c3
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js
@@ -0,0 +1,94 @@
+const {
+  Email,
+  services,
+  Fragment,
+  Collection,
+  Invitation,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, invitationId, fragmentId } = req.params
+  const { isAccepted, reason } = req.body
+
+  const UserModel = models.User
+  const user = await UserModel.find(req.user)
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+    fragment.invitations = fragment.invitations || []
+    const invitation = await fragment.invitations.find(
+      invitation => invitation.id === invitationId,
+    )
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'reviewer',
+    })
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
+      })
+
+    const collectionHelper = new Collection({ collection })
+    const fragmentHelper = new Fragment({ fragment })
+    const parsedFragment = await fragmentHelper.getFragmentData({
+      handlingEditor: collection.handlingEditor,
+    })
+    const baseUrl = services.getBaseUrl(req)
+    const {
+      authorsList: authors,
+      submittingAuthor,
+    } = await fragmentHelper.getAuthorData({ UserModel })
+    const emailHelper = new Email({
+      UserModel,
+      collection,
+      parsedFragment,
+      baseUrl,
+      authors,
+    })
+
+    invitation.respondedOn = Date.now()
+    invitation.hasAnswer = true
+    if (isAccepted) {
+      invitation.isAccepted = true
+      if (collection.status === 'reviewersInvited')
+        await collectionHelper.updateStatus({ newStatus: 'underReview' })
+      await fragment.save()
+
+      emailHelper.setupReviewerDecisionEmail({
+        agree: true,
+        timestamp: invitation.respondedOn,
+        user,
+        authorName: `${submittingAuthor.firstName} ${
+          submittingAuthor.lastName
+        }`,
+      })
+
+      return res.status(200).json(invitation)
+    }
+
+    invitation.isAccepted = false
+    if (reason) invitation.reason = reason
+    await fragment.save()
+
+    collectionHelper.updateStatusByNumberOfReviewers()
+    emailHelper.setupReviewerDecisionEmail({
+      agree: false,
+      user,
+    })
+
+    return res.status(200).json(invitation)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..38991ec08d033de565d5685d7ab6077b978d8504
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js
@@ -0,0 +1,159 @@
+const logger = require('@pubsweet/logger')
+const {
+  Email,
+  services,
+  authsome: authsomeHelper,
+  Fragment,
+  Collection,
+  Team,
+  Invitation,
+  User,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { email, role } = req.body
+
+  if (!services.checkForUndefinedParams(email, role)) {
+    res.status(400).json({ error: 'Email and role are required.' })
+    logger.error('User ID and role are missing')
+    return
+  }
+
+  if (role !== 'reviewer')
+    return res
+      .status(400)
+      .json({ error: `Role ${role} is invalid. Only reviewer is accepted.` })
+
+  const UserModel = models.User
+  const reqUser = await UserModel.find(req.user)
+
+  if (email === reqUser.email && !reqUser.admin)
+    return res.status(400).json({ error: 'Cannot invite yourself.' })
+
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+
+  const authsome = authsomeHelper.getAuthsome(models)
+  const target = {
+    collection,
+    path: req.route.path,
+  }
+  const canPost = await authsome.can(req.user, 'POST', target)
+  if (!canPost)
+    return res.status(403).json({
+      error: 'Unauthorized.',
+    })
+
+  const collectionHelper = new Collection({ collection })
+  const fragmentHelper = new Fragment({ fragment })
+  const handlingEditor = collection.handlingEditor || {}
+  const parsedFragment = await fragmentHelper.getFragmentData({
+    handlingEditor,
+  })
+  const baseUrl = services.getBaseUrl(req)
+  const {
+    authorsList: authors,
+    submittingAuthor,
+  } = await fragmentHelper.getAuthorData({ UserModel })
+  const emailHelper = new Email({
+    UserModel,
+    collection,
+    parsedFragment,
+    baseUrl,
+    authors,
+  })
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+    fragmentId,
+  })
+  const invitationHelper = new Invitation({ role })
+
+  try {
+    const user = await UserModel.findByEmail(email)
+    await teamHelper.setupTeam({ user, role, objectType: 'fragment' })
+    invitationHelper.userId = user.id
+
+    let invitation = invitationHelper.getInvitation({
+      invitations: fragment.invitations,
+    })
+    let resend = false
+
+    if (invitation) {
+      if (invitation.hasAnswer)
+        return res
+          .status(400)
+          .json({ error: 'User has already replied to a previous invitation.' })
+
+      invitation.invitedOn = Date.now()
+      await fragment.save()
+      resend = true
+    } else {
+      invitation = await invitationHelper.createInvitation({
+        parentObject: fragment,
+      })
+    }
+
+    if (collection.status === 'heAssigned')
+      await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
+
+    emailHelper.setupReviewerInvitationEmail({
+      user,
+      invitationId: invitation.id,
+      timestamp: invitation.invitedOn,
+      resend,
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+    })
+
+    return res.status(200).json(invitation)
+  } catch (e) {
+    const userHelper = new User({ UserModel })
+
+    const newUser = await userHelper.setupNewUser({
+      url: baseUrl,
+      role,
+      invitationType: 'invite',
+      body: req.body,
+    })
+
+    if (newUser.error !== undefined) {
+      return res.status(newUser.status).json({
+        error: newUser.message,
+      })
+    }
+
+    if (collection.status === 'heAssigned')
+      await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
+
+    await teamHelper.setupTeam({ user: newUser, role, objectType: 'fragment' })
+
+    invitationHelper.userId = newUser.id
+
+    const invitation = await invitationHelper.createInvitation({
+      parentObject: fragment,
+    })
+
+    emailHelper.setupReviewerInvitationEmail({
+      user: newUser,
+      invitationId: invitation.id,
+      timestamp: invitation.invitedOn,
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+    })
+
+    return res.status(200).json(invitation)
+  }
+}
diff --git a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js
index 4e6594490c6661e1db150e367b42f348997436bb..7fdef707ccadb721ce6099a69001da73ec29c552 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js
@@ -2,15 +2,15 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const cloneDeep = require('lodash/cloneDeep')
-const Model = require('./../helpers/Model')
-const fixtures = require('./../fixtures/fixtures')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
 }))
 
-const path = '../../routes/collectionsInvitations/delete'
+const path = '../routes/collectionsInvitations/delete'
 const route = {
   path: '/api/collections/:collectionId/invitations/:invitationId',
 }
@@ -35,7 +35,7 @@ describe('Delete Collections Invitations route handler', () => {
     })
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
+    expect(data.error).toEqual('Collection not found')
   })
   it('should return an error when the invitation does not exist', async () => {
     const { editorInChief } = testFixtures.users
diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
index 129b791c24ae9fdaf09f3f4bcc8573e1abff2985..7ed11f1546f5b45726c79c3ce683c0eeee0f776e 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
@@ -2,10 +2,10 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
   sendNotificationEmail: jest.fn(),
@@ -41,22 +41,6 @@ describe('Patch collections invitations route handler', () => {
     await require(patchPath)(models)(req, res)
     expect(res.statusCode).toBe(200)
   })
-  it('should return success when the reviewer agrees work on a collection', async () => {
-    const { reviewer } = testFixtures.users
-    const { collection } = testFixtures.collections
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = reviewer.id
-    req.params.collectionId = collection.id
-    const reviewerInv = collection.invitations.find(
-      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-    )
-    req.params.invitationId = reviewerInv.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-    expect(res.statusCode).toBe(200)
-  })
   it('should return success when the handling editor declines work on a collection', async () => {
     const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
@@ -75,41 +59,6 @@ describe('Patch collections invitations route handler', () => {
 
     expect(res.statusCode).toBe(200)
   })
-  it('should return success when the reviewer declines work on a collection', async () => {
-    const { reviewer } = testFixtures.users
-    const { collection } = testFixtures.collections
-    body.isAccepted = false
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = reviewer.id
-    req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
-      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-    )
-    req.params.invitationId = inv.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-  })
-  it('should return an error params are missing', async () => {
-    const { handlingEditor } = testFixtures.users
-    const { collection } = testFixtures.collections
-    delete body.isAccepted
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = handlingEditor.id
-    req.params.collectionId = collection.id
-    req.params.invitationId = collection.invitations[0].id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Missing parameters.')
-  })
   it('should return an error if the collection does not exists', async () => {
     const { handlingEditor } = testFixtures.users
     const req = httpMocks.createRequest({
diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
index 53eda4a07751aebb1120d5c00eadec0a60a87ae3..0608aced450fbf604c14fb2af509309262808ddc 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
@@ -1,15 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const random = require('lodash/random')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
-const config = require('config')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
-const configRoles = config.get('roles')
+const { Model, fixtures } = fixturesService
 
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
@@ -17,10 +14,9 @@ jest.mock('pubsweet-component-mail-service', () => ({
   sendReviewerInvitationEmail: jest.fn(),
 }))
 const chance = new Chance()
-const roles = configRoles.collection
 const reqBody = {
   email: chance.email(),
-  role: roles[random(0, roles.length - 1)],
+  role: 'handlingEditor',
   firstName: chance.first(),
   lastName: chance.last(),
   title: 'Mr',
@@ -31,7 +27,7 @@ const route = {
   path: '/api/collections/:collectionId/invitations',
 }
 
-const path = '../../routes/collectionsInvitations/post'
+const path = '../routes/collectionsInvitations/post'
 describe('Post collections invitations route handler', () => {
   let testFixtures = {}
   let body = {}
@@ -79,31 +75,8 @@ describe('Post collections invitations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.role).toEqual(body.role)
   })
-  it('should return success when the a reviewer is invited', async () => {
-    const { user, editorInChief } = testFixtures.users
-    const { collection } = testFixtures.collections
-    body = {
-      email: user.email,
-      role: 'reviewer',
-    }
-    const res = await requests.sendRequest({
-      body,
-      userId: editorInChief.id,
-      route,
-      models,
-      path,
-      params: {
-        collectionId: collection.id,
-      },
-    })
-
-    expect(res.statusCode).toBe(200)
-    const data = JSON.parse(res._getData())
-    expect(data.role).toEqual(body.role)
-  })
   it('should return an error when inviting his self', async () => {
     const { editorInChief } = testFixtures.users
-    body.role = roles[random(0, roles.length - 1)]
     body.email = editorInChief.email
     const res = await requests.sendRequest({
       body,
@@ -114,7 +87,7 @@ describe('Post collections invitations route handler', () => {
     })
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Cannot invite yourself')
+    expect(data.error).toEqual('Cannot invite yourself.')
   })
   it('should return an error when the role is invalid', async () => {
     const { editorInChief } = testFixtures.users
@@ -127,7 +100,9 @@ describe('Post collections invitations route handler', () => {
       path,
     })
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual(`Role ${body.role} is invalid`)
+    expect(data.error).toEqual(
+      `Role ${body.role} is invalid. Only handlingEditor is allowed.`,
+    )
   })
   it('should return success when the EiC resends an invitation to a handlingEditor with a collection', async () => {
     const { handlingEditor, editorInChief } = testFixtures.users
@@ -152,15 +127,15 @@ describe('Post collections invitations route handler', () => {
     expect(data.role).toEqual(body.role)
   })
   it('should return an error when the invitation is already answered', async () => {
-    const { answerReviewer, handlingEditor } = testFixtures.users
+    const { answerHE, editorInChief } = testFixtures.users
     const { collection } = testFixtures.collections
     body = {
-      email: answerReviewer.email,
-      role: 'reviewer',
+      email: answerHE.email,
+      role: 'handlingEditor',
     }
     const res = await requests.sendRequest({
       body,
-      userId: handlingEditor.id,
+      userId: editorInChief.id,
       route,
       models,
       path,
diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-invite/src/tests/fixtures/collections.js
deleted file mode 100644
index bef6132199981a79b87ea86440146eaab5891883..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/collections.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const Chance = require('chance')
-const {
-  user,
-  handlingEditor,
-  author,
-  reviewer,
-  answerReviewer,
-} = require('./userData')
-const { fragment } = require('./fragments')
-
-const chance = new Chance()
-const collections = {
-  collection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [fragment.id],
-    owners: [user.id],
-    save: jest.fn(),
-    authors: [
-      {
-        userId: author.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
-    invitations: [
-      {
-        id: chance.guid(),
-        role: 'handlingEditor',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: handlingEditor.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: reviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: true,
-        isAccepted: false,
-        userId: answerReviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: chance.timestamp(),
-      },
-    ],
-    handlingEditor: {
-      id: handlingEditor.id,
-      hasAnswer: false,
-      isAccepted: false,
-      email: handlingEditor.email,
-      invitedOn: chance.timestamp(),
-      respondedOn: null,
-      name: `${handlingEditor.firstName} ${handlingEditor.lastName}`,
-    },
-  },
-}
-
-module.exports = collections
diff --git a/packages/component-invite/src/tests/fixtures/fragments.js b/packages/component-invite/src/tests/fixtures/fragments.js
deleted file mode 100644
index 08d0eedf3a531c317cbfb79e58a6d1b019413564..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/fragments.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const fragments = {
-  fragment: {
-    id: chance.guid(),
-    metadata: {
-      title: chance.sentence(),
-      abstract: chance.paragraph(),
-    },
-    recommendations: [],
-  },
-}
-
-module.exports = fragments
diff --git a/packages/component-invite/src/tests/fixtures/teamIDs.js b/packages/component-invite/src/tests/fixtures/teamIDs.js
deleted file mode 100644
index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/teamIDs.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const heID = chance.guid()
-const revId = chance.guid()
-
-module.exports = {
-  heTeamID: heID,
-  revTeamID: revId,
-}
diff --git a/packages/component-invite/src/tests/fixtures/userData.js b/packages/component-invite/src/tests/fixtures/userData.js
deleted file mode 100644
index 546e2d867998c6de1cab8e10a4df20fb74d5288d..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/userData.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const generateUserData = () => ({
-  id: chance.guid(),
-  email: chance.email(),
-  firstName: chance.first(),
-  lastName: chance.last(),
-})
-
-module.exports = {
-  handlingEditor: generateUserData(),
-  user: generateUserData(),
-  admin: generateUserData(),
-  author: generateUserData(),
-  reviewer: generateUserData(),
-  answerReviewer: generateUserData(),
-}
diff --git a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
similarity index 73%
rename from packages/component-invite/src/tests/collectionsInvitations/decline.test.js
rename to packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
index 562c3d92981611c95a57664e281fe482a0b04fc3..ece17117d96e22a7c4c7cf2288d537c18b4bf788 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js
+++ b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
@@ -2,8 +2,9 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
 const cloneDeep = require('lodash/cloneDeep')
 
 jest.mock('pubsweet-component-mail-service', () => ({
@@ -13,8 +14,8 @@ jest.mock('pubsweet-component-mail-service', () => ({
 const reqBody = {
   invitationToken: 'inv-token-123',
 }
-const patchPath = '../../routes/collectionsInvitations/decline'
-describe('Patch collections invitations route handler', () => {
+const patchPath = '../../routes/fragmentsInvitations/decline'
+describe('Decline fragments invitations route handler', () => {
   let testFixtures = {}
   let body = {}
   let models
@@ -26,12 +27,16 @@ describe('Patch collections invitations route handler', () => {
   it('should return success when the reviewer declines work on a collection', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
       inv => inv.role === 'reviewer' && inv.hasAnswer === false,
     )
     req.params.invitationId = inv.id
@@ -42,13 +47,14 @@ describe('Patch collections invitations route handler', () => {
   it('should return an error params are missing', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+
     delete body.invitationToken
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    req.params.invitationId = collection.invitations[0].id
+
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
 
@@ -70,32 +76,59 @@ describe('Patch collections invitations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('item not found')
   })
+  it('should return an error if the fragment does not exists', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-id'
+
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}`,
+    )
+  })
   it('should return an error when the invitation does not exist', async () => {
     const { user } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
     const req = httpMocks.createRequest({
       body,
     })
     req.user = user.id
     req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
     req.params.invitationId = 'invalid-id'
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
 
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Invitation invalid-id not found')
+    expect(data.error).toEqual('Invitation not found.')
   })
   it('should return an error when the token is invalid', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     body.invitationToken = 'invalid-token'
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
       inv => inv.role === 'reviewer' && inv.hasAnswer === false,
     )
     req.params.invitationId = inv.id
@@ -108,12 +141,16 @@ describe('Patch collections invitations route handler', () => {
   it('should return an error when the invitation is already answered', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(inv => inv.hasAnswer)
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(inv => inv.hasAnswer)
     req.params.invitationId = inv.id
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..8396173f8262ce7698b2a1ddbed0c17b6fcb7b60
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js
@@ -0,0 +1,117 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+}))
+
+const path = '../routes/fragmentsInvitations/delete'
+const route = {
+  path:
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId',
+}
+
+describe('Delete Fragments Invitations route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('collection not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}.`,
+    )
+  })
+  it('should return an error when the invitation does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation invalid-id not found')
+  })
+  it('should return success when the collection and invitation exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: fragment.invitations[0].id,
+      },
+    })
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the user does not have invitation rights', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      userId: user.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: collection.invitations[0].id,
+      },
+    })
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+})
diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js
similarity index 65%
rename from packages/component-invite/src/tests/collectionsInvitations/get.test.js
rename to packages/component-invite/src/tests/fragmentsInvitations/get.test.js
index 7cf8a64115d3cfd58db7db627638ee37d296f43d..043e162fac230e383f769e197ec47ca0ba8debf9 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js
+++ b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js
@@ -1,21 +1,22 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
   sendNotificationEmail: jest.fn(),
   sendReviewerInvitationEmail: jest.fn(),
 }))
-const path = '../../routes/collectionsInvitations/get'
+const path = '../routes/fragmentsInvitations/get'
 const route = {
-  path: '/api/collections/:collectionId/invitations/:invitationId?',
+  path:
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId?',
 }
-describe('Get collection invitations route handler', () => {
+describe('Get fragment invitations route handler', () => {
   let testFixtures = {}
   let models
   beforeEach(() => {
@@ -23,19 +24,21 @@ describe('Get collection invitations route handler', () => {
     models = Model.build(testFixtures)
   })
   it('should return success when the request data is correct', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'handlingEditor',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
 
@@ -44,9 +47,9 @@ describe('Get collection invitations route handler', () => {
     expect(data.length).toBeGreaterThan(0)
   })
   it('should return an error when parameters are missing', async () => {
-    const { editorInChief } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
@@ -56,15 +59,14 @@ describe('Get collection invitations route handler', () => {
     expect(data.error).toEqual('Role is required')
   })
   it('should return an error when the collection does not exist', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'handlingEditor',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: 'invalid-id',
@@ -72,54 +74,59 @@ describe('Get collection invitations route handler', () => {
     })
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
+    expect(data.error).toEqual('Item not found')
   })
-  it('should return an error when the role is invalid', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+  it('should return an error when the fragment does not exist', async () => {
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'invalidRole',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: 'invalid-id',
       },
     })
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual(`Role invalidRole is invalid`)
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}.`,
+    )
   })
-  it('should return success with an empty array when the collection does not have a the requested role team', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+  it('should return an error when the role is invalid', async () => {
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
-    delete collection.invitations
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'author',
-        userId: handlingEditor.id,
+        role: 'invalidRole',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
-    expect(res.statusCode).toBe(200)
+    expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data).toHaveLength(0)
+    expect(data.error).toEqual(`Role invalidRole is invalid`)
   })
   it('should return an error when a user does not have invitation rights', async () => {
-    const { author } = testFixtures.users
+    const { user } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: author.id,
+      userId: user.id,
       route,
       models,
       path,
@@ -128,6 +135,7 @@ describe('Get collection invitations route handler', () => {
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
     expect(res.statusCode).toBe(403)
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..93006adf157c73b8eff63846e335c3bdb8d46a23
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js
@@ -0,0 +1,160 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+  sendReviewerInvitationEmail: jest.fn(),
+}))
+
+const reqBody = {
+  isAccepted: true,
+}
+const patchPath = '../../routes/fragmentsInvitations/patch'
+describe('Patch fragments invitations route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the reviewer agrees work on a collection', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    const reviewerInv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = reviewerInv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return success when the reviewer declines work on a collection', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    body.isAccepted = false
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error if the collection does not exists', async () => {
+    const { handlingEditor } = testFixtures.users
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Item not found')
+  })
+  it('should return an error when the invitation does not exist', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = user.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    req.params.invitationId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation not found.')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = user.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    req.params.invitationId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation not found.')
+  })
+  it("should return an error when a user tries to patch another user's invitation", async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    const inv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(`User is not allowed to modify this invitation.`)
+  })
+  it('should return an error when the invitation is already answered', async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(inv => inv.hasAnswer)
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(`Invitation has already been answered.`)
+  })
+})
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..21da70379fe9ba9dc1e06df3802f6a331000a6fb
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js
@@ -0,0 +1,155 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const Chance = require('chance')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+  sendReviewerInvitationEmail: jest.fn(),
+}))
+const chance = new Chance()
+const reqBody = {
+  email: chance.email(),
+  role: 'reviewer',
+  firstName: chance.first(),
+  lastName: chance.last(),
+  title: 'Mr',
+  affiliation: chance.company(),
+  admin: false,
+}
+const route = {
+  path: '/api/collections/:collectionId/fragments/:fragmentId/invitations',
+}
+
+const path = '../routes/fragmentsInvitations/post'
+describe('Post fragments invitations route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return an error params are missing', async () => {
+    const { admin } = testFixtures.users
+    delete body.email
+    const res = await requests.sendRequest({
+      body,
+      userId: admin.id,
+      route,
+      models,
+      path,
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Email and role are required.')
+  })
+  it('should return success when the a reviewer is invited', async () => {
+    const { user, editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    body = {
+      email: user.email,
+      role: 'reviewer',
+    }
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+    expect(data.role).toEqual(body.role)
+  })
+  it('should return an error when inviting his self', async () => {
+    const { editorInChief } = testFixtures.users
+    body.email = editorInChief.email
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Cannot invite yourself.')
+  })
+  it('should return an error when the role is invalid', async () => {
+    const { editorInChief } = testFixtures.users
+    body.role = 'someRandomRole'
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+    })
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Role ${body.role} is invalid. Only reviewer is accepted.`,
+    )
+  })
+  it('should return an error when the invitation is already answered', async () => {
+    const { answerReviewer, handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body = {
+      email: answerReviewer.email,
+      role: 'reviewer',
+    }
+    const res = await requests.sendRequest({
+      body,
+      userId: handlingEditor.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `User has already replied to a previous invitation.`,
+    )
+  })
+  it('should return an error when the user does not have invitation rights', async () => {
+    const { author } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: author.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+})
diff --git a/packages/component-invite/src/tests/helpers/requests.js b/packages/component-invite/src/tests/requests.js
similarity index 100%
rename from packages/component-invite/src/tests/helpers/requests.js
rename to packages/component-invite/src/tests/requests.js
diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js
index 69ffa1481fd9b4562d66d646f8b553654aff8c50..d40d99d1f2803362450ef95ec51552948ce6e791 100644
--- a/packages/component-mail-service/src/Mail.js
+++ b/packages/component-mail-service/src/Mail.js
@@ -522,6 +522,25 @@ module.exports = {
           replacements.signatureName
         }`
         break
+      case 'he-manuscript-return-with-comments':
+        subject = meta.emailSubject
+        replacements.hasLink = false
+        replacements.previewText =
+          'a manuscript has been returned with comments'
+        replacements.intro = `Dear Dr. ${meta.handlingEditorName}`
+
+        replacements.paragraph = `Thank you for your recommendation for the manuscript titled "${
+          meta.fragment.title
+        }" by ${
+          meta.fragment.authorName
+        } based on the reviews you received.<br/><br/>
+        ${meta.eicComments}<br/><br/>`
+        delete replacements.detailsUrl
+        replacements.signatureName = meta.eicName
+        textBody = `${replacements.intro} ${replacements.paragraph} ${
+          replacements.signatureName
+        }`
+        break
       case 'submitting-reviewers-after-decision':
         subject = meta.emailSubject
         replacements.hasLink = false
diff --git a/packages/component-manuscript-manager/config/authsome-helpers.js b/packages/component-manuscript-manager/config/authsome-helpers.js
index 1add8d99b7fb8eca56a2abb7bf43088d599e5efc..658f47bb662ab0e414324831966daae0bf525f0c 100644
--- a/packages/component-manuscript-manager/config/authsome-helpers.js
+++ b/packages/component-manuscript-manager/config/authsome-helpers.js
@@ -44,7 +44,10 @@ const filterObjectData = (
           rec => rec.userId === user.id,
         )
     }
-
+    parseAuthorsData(object, matchingCollPerm)
+    if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
+      return filterRefusedInvitations(object, user)
+    }
     return object
   }
   const matchingCollPerm = collectionsPermissions.find(
@@ -52,32 +55,76 @@ const filterObjectData = (
   )
   if (matchingCollPerm === undefined) return null
   setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
 
   return object
 }
 
-const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
-  const teams = await Promise.all(
+const getTeamsByPermissions = async (
+  teamIds = [],
+  permissions = [],
+  TeamModel,
+) =>
+  (await Promise.all(
     teamIds.map(async teamId => {
       const team = await TeamModel.find(teamId)
-      if (permissions.includes(team.teamType.permissions)) {
-        return team
+      if (!permissions.includes(team.teamType.permissions)) {
+        return null
       }
-      return null
+      return team
     }),
+  )).filter(Boolean)
+
+const heIsInvitedToFragment = async ({ user, Team, collectionId }) =>
+  (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some(
+    // user is a member of the team with access to the fragment's parent collection
+    t => t.members.includes(user.id) && t.object.id === collectionId,
   )
 
-  return teams.filter(Boolean)
+const getUserPermissions = async ({
+  user,
+  Team,
+  mapFn = t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }),
+}) =>
+  (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn)
+
+const isOwner = ({ user: { id }, object }) => {
+  if (object.owners.includes(id)) return true
+  return !!object.owners.find(own => own.id === id)
+}
+
+const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => {
+  const userPermissions = await getUserPermissions({
+    user,
+    Team,
+  })
+
+  return !!userPermissions.find(p => {
+    const hasObject =
+      p.objectId === get(object, 'fragment.id') ||
+      p.objectId === get(object, 'fragment.collectionId')
+    if (roles.length > 0) {
+      return hasObject && roles.includes(p.role)
+    }
+    return hasObject
+  })
 }
 
+const isHandlingEditor = ({ user, object }) =>
+  get(object, 'collection.handlingEditor.id') === user.id
+
 module.exports = {
+  filterObjectData,
   parseAuthorsData,
   setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
   getTeamsByPermissions,
+  filterRefusedInvitations,
+  isOwner,
+  isHandlingEditor,
+  getUserPermissions,
+  heIsInvitedToFragment,
+  hasPermissionForObject,
 }
diff --git a/packages/component-manuscript-manager/config/authsome-mode.js b/packages/component-manuscript-manager/config/authsome-mode.js
index 667879b274b86a59a28515ed3d595d97e6ad2808..2c80868ddf05885664bdaa937bcf571bb32d264a 100644
--- a/packages/component-manuscript-manager/config/authsome-mode.js
+++ b/packages/component-manuscript-manager/config/authsome-mode.js
@@ -1,52 +1,8 @@
-const get = require('lodash/get')
-const pickBy = require('lodash/pickBy')
-const omit = require('lodash/omit')
-const helpers = require('./authsome-helpers')
-
-async function teamPermissions(user, operation, object, context) {
-  const permissions = ['handlingEditor', 'author', 'reviewer']
-  const teams = await helpers.getTeamsByPermissions(
-    user.teams,
-    permissions,
-    context.models.Team,
-  )
-
-  const collectionsPermissions = await Promise.all(
-    teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
-      const collPerm = {
-        id: collection.id,
-        permission: team.teamType.permissions,
-      }
-      const objectType = get(object, 'type')
-      if (objectType === 'fragment' && collection.fragments.includes(object.id))
-        collPerm.fragmentId = object.id
-
-      return collPerm
-    }),
-  )
-
-  if (collectionsPermissions.length === 0) return {}
-
-  return {
-    filter: filterParam => {
-      if (!filterParam.length) {
-        return helpers.filterObjectData(
-          collectionsPermissions,
-          filterParam,
-          user,
-        )
-      }
+const config = require('config')
+const { get, pickBy, omit } = require('lodash')
 
-      const collections = filterParam
-        .map(coll =>
-          helpers.filterObjectData(collectionsPermissions, coll, user),
-        )
-        .filter(Boolean)
-      return collections
-    },
-  }
-}
+const statuses = config.get('statuses')
+const helpers = require('./authsome-helpers')
 
 function unauthenticatedUser(operation, object) {
   // Public/unauthenticated users can GET /collections, filtered by 'published'
@@ -94,137 +50,198 @@ function unauthenticatedUser(operation, object) {
   return false
 }
 
+const publicStatusesPermissions = ['author', 'reviewer']
+const createPaths = ['/collections', '/collections/:collectionId/fragments']
+
 async function authenticatedUser(user, operation, object, context) {
-  // Allow the authenticated user to POST a collection (but not with a 'filtered' property)
-  if (operation === 'POST' && object.path === '/collections') {
-    return {
-      filter: collection => omit(collection, 'filtered'),
+  if (operation === 'GET') {
+    if (get(object, 'path') === '/collections') {
+      return {
+        filter: async collections => {
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          return collections.filter(collection => {
+            if (collection.owners.includes(user.id)) {
+              return true
+            }
+            const collectionPermission = userPermissions.find(
+              p => p.objectId === collection.id,
+            )
+            if (collectionPermission) {
+              return true
+            }
+
+            const fragmentPermission = userPermissions.find(p =>
+              collection.fragments.includes(p.objectId),
+            )
+            if (fragmentPermission) {
+              return true
+            }
+            return false
+          })
+        },
+      }
     }
-  }
 
-  if (
-    operation === 'POST' &&
-    object.path === '/collections/:collectionId/fragments'
-  ) {
-    return true
-  }
+    if (object === '/users') {
+      return true
+    }
 
-  // Allow the authenticated user to GET collections they own
-  if (operation === 'GET' && object === '/collections/') {
-    return {
-      filter: collection => collection.owners.includes(user.id),
+    if (get(object, 'type') === 'collection') {
+      if (helpers.isOwner({ user, object })) {
+        return true
+      }
+      return {
+        filter: async collection => {
+          const status = get(collection, 'status') || 'draft'
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          if (collection.owners.map(o => o.id).includes(user.id)) {
+            return collection
+          }
+
+          const collectionPermission = userPermissions.find(
+            p => p.objectId === collection.id,
+          )
+          if (
+            publicStatusesPermissions.includes(
+              get(collectionPermission, 'role'),
+            )
+          ) {
+            collection.visibleStatus = statuses[status].public
+          }
+          return collection
+        },
+      }
     }
-  }
 
-  // Allow owners of a collection to GET its teams, e.g.
-  // GET /api/collections/1/teams
-  if (operation === 'GET' && get(object, 'path') === '/teams') {
-    const collectionId = get(object, 'params.collectionId')
-    if (collectionId) {
-      const collection = await context.models.Collection.find(collectionId)
-      if (collection.owners.includes(user.id)) {
+    if (get(object, 'type') === 'fragment') {
+      if (helpers.isOwner({ user, object })) {
         return true
       }
+
+      const userPermissions = await helpers.getUserPermissions({
+        user,
+        Team: context.models.Team,
+      })
+
+      const permission = userPermissions.find(
+        p => p.objectId === object.id || p.objectId === object.collectionId,
+      )
+
+      if (!permission) return false
+
+      return {
+        filter: fragment => {
+          // handle other roles
+          if (permission.role === 'reviewer') {
+            fragment.files = omit(fragment.files, ['coverLetter'])
+            fragment.authors = fragment.authors.map(a => omit(a, ['email']))
+          }
+          return fragment
+        },
+      }
     }
-  }
 
-  if (
-    operation === 'GET' &&
-    get(object, 'type') === 'team' &&
-    get(object, 'object.type') === 'collection'
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object, 'object.id'),
-    )
-    if (collection.owners.includes(user.id)) {
+    if (get(object, 'type') === 'user') {
       return true
     }
-  }
 
-  // Advanced example
-  // Allow authenticated users to create a team based around a collection
-  // if they are one of the owners of this collection
-  if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') {
-    if (get(object, 'object.type') === 'collection') {
-      const collection = await context.models.Collection.find(
-        get(object, 'object.id'),
-      )
-      if (collection.owners.includes(user.id)) {
+    // allow HE to get reviewer invitations
+    if (get(object, 'fragment.type') === 'fragment') {
+      const collectionId = get(object, 'fragment.collectionId')
+      const collection = await context.models.Collection.find(collectionId)
+
+      if (get(collection, 'handlingEditor.id') === user.id) {
         return true
       }
     }
-  }
 
-  // only allow the HE to create, delete an invitation, or get invitation details
-  if (
-    ['POST', 'GET', 'DELETE'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('invitations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const handlingEditor = get(collection, 'handlingEditor')
-    if (!handlingEditor) return false
-    if (handlingEditor.id === user.id) return true
-    return false
+    if (get(object, 'type') === 'user') {
+      return true
+    }
   }
 
-  // only allow a reviewer and an HE to submit and to modify a recommendation
-  if (
-    ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('recommendations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const teams = await helpers.getTeamsByPermissions(
-      user.teams,
-      ['reviewer', 'handlingEditor'],
-      context.models.Team,
-    )
-    if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
-    if (matchingTeam) return true
-    return false
-  }
+  if (operation === 'POST') {
+    // allow everytone to create manuscripts and versions
+    if (createPaths.includes(object.path)) {
+      return true
+    }
 
-  if (user.teams.length !== 0 && operation === 'GET') {
-    const permissions = await teamPermissions(user, operation, object, context)
+    // allow HE to invite
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
 
-    if (permissions) {
-      return permissions
+    // allow HE or assigned reviewers to recommend
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer', 'handlingEditor'],
+      })
     }
   }
 
-  if (get(object, 'type') === 'fragment') {
-    const fragment = object
+  if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
+    }
 
-    if (fragment.owners.includes(user.id)) {
-      return true
+    if (get(object, 'type') === 'fragment') {
+      return helpers.isOwner({ user, object })
     }
-  }
 
-  if (get(object, 'type') === 'collection') {
-    if (['GET', 'DELETE'].includes(operation)) {
+    // allow reviewer to patch his recommendation
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer'],
+      })
+    }
+
+    if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
       return true
     }
 
-    // Only allow filtered updating (mirroring filtered creation) for non-admin users)
-    if (operation === 'PATCH') {
-      return {
-        filter: collection => omit(collection, 'filtered'),
-      }
+    // allow owner to submit a revision
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/submit'
+    ) {
+      return helpers.isOwner({ user, object: object.fragment })
     }
   }
 
-  // A user can GET, DELETE and PATCH itself
-  if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
-    if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
-      return true
+  if (operation === 'DELETE') {
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
+
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
     }
   }
+
   // If no individual permissions exist (above), fallback to unauthenticated
   // user's permission
   return unauthenticatedUser(operation, object)
@@ -240,7 +257,7 @@ const authsomeMode = async (userId, operation, object, context) => {
   const user = await context.models.User.find(userId)
 
   // Admins and editor in chiefs can do anything
-  if (user && (user.admin === true || user.editorInChief === true)) return true
+  if (user && (user.admin || user.editorInChief)) return true
 
   if (user) {
     return authenticatedUser(user, operation, object, context)
diff --git a/packages/component-manuscript-manager/config/default.js b/packages/component-manuscript-manager/config/default.js
index 276960b2fa5ce4133c55e91766a40dc4f69ea69f..241cb498d07eda9741434edeffd69545a01e56a0 100644
--- a/packages/component-manuscript-manager/config/default.js
+++ b/packages/component-manuscript-manager/config/default.js
@@ -40,11 +40,11 @@ module.exports = {
     },
     heInvited: {
       public: 'Submitted',
-      private: 'HE Invited',
+      private: 'Handling Editor Invited',
     },
     heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
+      public: 'Handling Editor Assigned',
+      private: 'Handling Editor Assigned',
     },
     reviewersInvited: {
       public: 'Reviewers Invited',
@@ -54,10 +54,18 @@ module.exports = {
       public: 'Under Review',
       private: 'Under Review',
     },
+    reviewCompleted: {
+      public: 'Under Review',
+      private: 'Review Completed',
+    },
     pendingApproval: {
       public: 'Under Review',
       private: 'Pending Approval',
     },
+    revisionRequested: {
+      public: 'Revision Requested',
+      private: 'Revision Requested',
+    },
     rejected: {
       public: 'Rejected',
       private: 'Rejected',
diff --git a/packages/component-manuscript-manager/config/test.js b/packages/component-manuscript-manager/config/test.js
index 6869d659a3af2b1b643561889f4f81d4c486d1cf..9dad34bbfb2175e75ee07570b88965415556241f 100644
--- a/packages/component-manuscript-manager/config/test.js
+++ b/packages/component-manuscript-manager/config/test.js
@@ -41,11 +41,11 @@ module.exports = {
     },
     heInvited: {
       public: 'Submitted',
-      private: 'HE Invited',
+      private: 'Handling Editor Invited',
     },
     heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
+      public: 'Handling Editor Assigned',
+      private: 'Handling Editor Assigned',
     },
     reviewersInvited: {
       public: 'Reviewers Invited',
@@ -55,10 +55,18 @@ module.exports = {
       public: 'Under Review',
       private: 'Under Review',
     },
+    reviewCompleted: {
+      public: 'Under Review',
+      private: 'Review Completed',
+    },
     pendingApproval: {
       public: 'Under Review',
       private: 'Pending Approval',
     },
+    revisionRequested: {
+      public: 'Revision Requested',
+      private: 'Revision Requested',
+    },
     rejected: {
       public: 'Rejected',
       private: 'Rejected',
diff --git a/packages/component-manuscript-manager/index.js b/packages/component-manuscript-manager/index.js
index 0c04f744119898af8a4ae1dc44a6eabbc5c2b24b..63ee7dbde1665993000139f654d8669b7465f8cb 100644
--- a/packages/component-manuscript-manager/index.js
+++ b/packages/component-manuscript-manager/index.js
@@ -1,5 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/FragmentsRecommendations')(app)
+    require('./src/Fragments')(app)
   },
 }
diff --git a/packages/component-manuscript-manager/src/Fragments.js b/packages/component-manuscript-manager/src/Fragments.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e3569166d7dd7e7e76f9082412ab3bc8219c16a
--- /dev/null
+++ b/packages/component-manuscript-manager/src/Fragments.js
@@ -0,0 +1,33 @@
+const bodyParser = require('body-parser')
+
+const Fragments = app => {
+  app.use(bodyParser.json())
+  const basePath = '/api/collections/:collectionId/fragments/:fragmentId/submit'
+  const routePath = './routes/fragments'
+  const authBearer = app.locals.passport.authenticate('bearer', {
+    session: false,
+  })
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a revision for a manuscript
+   * @apiGroup FragmentsRecommendations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiSuccessExample {json} Success
+   *   HTTP/1.1 200 OK
+   *   {
+   *
+   *   }
+   * @apiErrorExample {json} Invite user errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}`,
+    authBearer,
+    require(`${routePath}/patch`)(app.locals.models),
+  )
+}
+
+module.exports = Fragments
diff --git a/packages/component-manuscript-manager/src/routes/fragments/patch.js b/packages/component-manuscript-manager/src/routes/fragments/patch.js
new file mode 100644
index 0000000000000000000000000000000000000000..2dfa426ea13a3fad0cd3a3b36d2e5248280990d7
--- /dev/null
+++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js
@@ -0,0 +1,52 @@
+const {
+  services,
+  Fragment,
+  Collection,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Collection and fragment do not match.`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      fragment,
+      path: req.route.path,
+    }
+    const canPatch = await authsome.can(req.user, 'PATCH', target)
+    if (!canPatch)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+
+    const collectionHelper = new Collection({ collection })
+    const fragmentHelper = new Fragment({ fragment })
+
+    const heRecommendation = fragmentHelper.getHeRequestToRevision()
+    if (!heRecommendation) {
+      return res.status(400).json({
+        error: 'No Handling Editor recommendation has been found.',
+      })
+    }
+
+    collectionHelper.updateStatusByRecommendation({
+      recommendation: heRecommendation.recommendation,
+    })
+
+    return res.status(200).json(fragment)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
index fc771b67cbee2448a50bbad958be986c0c7b96d7..39ac00b46fca295bab740136fa1160f7d0f193d4 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
@@ -5,48 +5,45 @@ const {
   Fragment,
   Collection,
 } = require('pubsweet-component-helper-service')
-const logger = require('@pubsweet/logger')
 
 module.exports = models => async (req, res) => {
   const { collectionId, fragmentId, recommendationId } = req.params
   let collection, fragment
   try {
     collection = await models.Collection.find(collectionId)
-    if (!collection.fragments.includes(fragmentId)) {
-      logger.error(
-        `Collection ${collectionId} does not contain fragment ${fragmentId}`,
-      )
+    if (!collection.fragments.includes(fragmentId))
       return res.status(400).json({
         error: `Collection and fragment do not match.`,
       })
-    }
-    const authsome = authsomeHelper.getAuthsome(models)
-    const target = {
-      collection,
-      path: req.route.path,
-    }
-    const UserModel = models.User
-    const user = await UserModel.find(req.user)
-    const canPatch = await authsome.can(req.user, 'PATCH', target)
-    if (!canPatch) {
-      logger.error(
-        `User ${req.user} is not allowed to access Collection ${collectionId}`,
-      )
-      return res.status(403).json({
-        error: 'Unauthorized.',
-      })
-    }
 
     fragment = await models.Fragment.find(fragmentId)
+
     const recommendation = fragment.recommendations.find(
       rec => rec.id === recommendationId,
     )
+
     if (!recommendation)
       return res.status(404).json({ error: 'Recommendation not found.' })
+
     if (recommendation.userId !== req.user)
       return res.status(403).json({
         error: 'Unauthorized.',
       })
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      fragment,
+      path: req.route.path,
+    }
+    const canPatch = await authsome.can(req.user, 'PATCH', target)
+    if (!canPatch)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+
+    const UserModel = models.User
+    const user = await UserModel.find(req.user)
+
     Object.assign(recommendation, req.body)
     recommendation.updatedOn = Date.now()
     if (req.body.submittedOn) {
@@ -56,9 +53,11 @@ module.exports = models => async (req, res) => {
       })
       const baseUrl = services.getBaseUrl(req)
       const collectionHelper = new Collection({ collection })
-      const authors = await collectionHelper.getAuthorData({
+
+      const authors = await fragmentHelper.getAuthorData({
         UserModel,
       })
+
       const email = new Email({
         UserModel,
         collection,
@@ -66,14 +65,16 @@ module.exports = models => async (req, res) => {
         baseUrl,
         authors,
       })
+
       email.setupHandlingEditorEmail({
         reviewSubmitted: true,
         reviewerName: `${user.firstName} ${user.lastName}`,
       })
+
       if (!['pendingApproval', 'revisionRequested'].includes(collection.status))
         collectionHelper.updateStatus({ newStatus: 'reviewCompleted' })
     }
-    await fragment.save()
+    fragment.save()
     return res.status(200).json(recommendation)
   } catch (e) {
     const notFoundError = await services.handleNotFoundError(e, 'Item')
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index d5a4a500385cc43e771d01758a78137236320ad7..53ae3e90411ee519901840258d8776a40ef9aeee 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -1,4 +1,5 @@
 const uuid = require('uuid')
+const { chain } = require('lodash')
 const {
   Email,
   services,
@@ -23,7 +24,6 @@ module.exports = models => async (req, res) => {
       return res.status(400).json({
         error: `Collection and fragment do not match.`,
       })
-
     fragment = await models.Fragment.find(fragmentId)
   } catch (e) {
     const notFoundError = await services.handleNotFoundError(e, 'Item')
@@ -31,9 +31,10 @@ module.exports = models => async (req, res) => {
       error: notFoundError.message,
     })
   }
+
   const authsome = authsomeHelper.getAuthsome(models)
   const target = {
-    collection,
+    fragment,
     path: req.route.path,
   }
   const canPost = await authsome.can(req.user, 'POST', target)
@@ -41,6 +42,7 @@ module.exports = models => async (req, res) => {
     return res.status(403).json({
       error: 'Unauthorized.',
     })
+
   fragment.recommendations = fragment.recommendations || []
   const newRecommendation = {
     id: uuid.v4(),
@@ -58,8 +60,10 @@ module.exports = models => async (req, res) => {
   const parsedFragment = await fragmentHelper.getFragmentData({
     handlingEditor: collection.handlingEditor,
   })
+
   const baseUrl = services.getBaseUrl(req)
-  const authors = await collectionHelper.getAuthorData({ UserModel })
+  const authors = await fragmentHelper.getAuthorData({ UserModel })
+
   const email = new Email({
     UserModel,
     collection,
@@ -67,17 +71,28 @@ module.exports = models => async (req, res) => {
     baseUrl,
     authors,
   })
+  const FragmentModel = models.Fragment
 
   if (reqUser.editorInChief || reqUser.admin) {
-    if (recommendation === 'return-to-handling-editor')
+    if (recommendation === 'return-to-handling-editor') {
       collectionHelper.updateStatus({ newStatus: 'reviewCompleted' })
-    else {
+      const eicComments = chain(newRecommendation)
+        .get('comments')
+        .find(comm => !comm.public)
+        .get('content')
+        .value()
+      email.parsedFragment.eicComments = eicComments
+      email.setupHandlingEditorEmail({
+        returnWithComments: true,
+      })
+    } else {
       collectionHelper.updateFinalStatusByRecommendation({
         recommendation,
       })
       email.setupAuthorsEmail({
         requestToRevision: false,
         publish: recommendation === 'publish',
+        FragmentModel,
       })
       email.setupHandlingEditorEmail({
         publish: recommendation === 'publish',
@@ -87,15 +102,20 @@ module.exports = models => async (req, res) => {
         recommendation,
         isSubmitted: true,
         agree: true,
+        FragmentModel,
       })
     }
   } else if (recommendationType === 'editorRecommendation') {
-    collectionHelper.updateStatusByRecommendation({ recommendation })
+    collectionHelper.updateStatusByRecommendation({
+      recommendation,
+      isHandlingEditor: true,
+    })
     email.setupReviewersEmail({
       recommendation,
       agree: true,
+      FragmentModel,
     })
-    email.setupReviewersEmail({ agree: false })
+    email.setupReviewersEmail({ agree: false, FragmentModel: models.Fragment })
     email.setupEiCEmail({
       recommendation,
       comments: newRecommendation.comments,
@@ -107,7 +127,6 @@ module.exports = models => async (req, res) => {
       })
     }
   }
-
   fragment.recommendations.push(newRecommendation)
   await fragment.save()
   return res.status(200).json(newRecommendation)
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js b/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js
deleted file mode 100644
index cbb5c85cbe056c7dc633db935bf5707764ac40bd..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const fragments = require('./fragments')
-const teams = require('./teams')
-
-module.exports = {
-  users,
-  collections,
-  fragments,
-  teams,
-}
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js b/packages/component-manuscript-manager/src/tests/fixtures/fragments.js
deleted file mode 100644
index 0bc2a06b2cacf0a2a2dcb83ee5202cf9059f3618..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const Chance = require('chance')
-const { recReviewer } = require('./userData')
-
-const chance = new Chance()
-const fragments = {
-  fragment: {
-    id: chance.guid(),
-    metadata: {
-      title: chance.sentence(),
-      abstract: chance.paragraph(),
-    },
-    save: jest.fn(),
-    recommendations: [
-      {
-        recommendation: 'accept',
-        recommendationType: 'review',
-        comments: [
-          {
-            content: chance.paragraph(),
-            public: chance.bool(),
-            files: [
-              {
-                id: chance.guid(),
-                name: 'file.pdf',
-                size: chance.natural(),
-              },
-            ],
-          },
-        ],
-        id: chance.guid(),
-        userId: recReviewer.id,
-        createdOn: chance.timestamp(),
-        updatedOn: chance.timestamp(),
-      },
-    ],
-  },
-}
-
-module.exports = fragments
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js b/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js
deleted file mode 100644
index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const heID = chance.guid()
-const revId = chance.guid()
-
-module.exports = {
-  heTeamID: heID,
-  revTeamID: revId,
-}
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teams.js b/packages/component-manuscript-manager/src/tests/fixtures/teams.js
deleted file mode 100644
index 1c87e804343747e1dfa7132259bc044c9f39081e..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/teams.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const { revTeamID, heTeamID } = require('./teamIDs')
-
-const { collection } = collections
-const { reviewer, handlingEditor } = users
-const teams = {
-  revTeam: {
-    teamType: {
-      name: 'reviewer',
-      permissions: 'reviewer',
-    },
-    group: 'reviewer',
-    name: 'reviewer',
-    object: {
-      type: 'collection',
-      id: collection.id,
-    },
-    members: [reviewer.id],
-    save: jest.fn(() => teams.revTeam),
-    updateProperties: jest.fn(() => teams.revTeam),
-    id: revTeamID,
-  },
-  heTeam: {
-    teamType: {
-      name: 'handlingEditor',
-      permissions: 'handlingEditor',
-    },
-    group: 'handlingEditor',
-    name: 'HandlingEditor',
-    object: {
-      type: 'collection',
-      id: collection.id,
-    },
-    members: [handlingEditor.id],
-    save: jest.fn(() => teams.heTeam),
-    updateProperties: jest.fn(() => teams.heTeam),
-    id: heTeamID,
-  },
-}
-module.exports = teams
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/users.js b/packages/component-manuscript-manager/src/tests/fixtures/users.js
deleted file mode 100644
index 573c25543f255e06ffe659e1dcc5bd3feb2d38cb..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/users.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const { reviewer, author, recReviewer, handlingEditor } = require('./userData')
-const { revTeamID, heTeamID } = require('./teamIDs')
-
-const Chance = require('chance')
-
-const chance = new Chance()
-const users = {
-  reviewer: {
-    type: 'user',
-    username: chance.word(),
-    email: reviewer.email,
-    password: 'password',
-    admin: false,
-    id: reviewer.id,
-    firstName: reviewer.firstName,
-    lastName: reviewer.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.reviewer),
-    isConfirmed: true,
-    teams: [revTeamID],
-  },
-  author: {
-    type: 'user',
-    username: chance.word(),
-    email: author.email,
-    password: 'password',
-    admin: false,
-    id: author.id,
-    firstName: author.firstName,
-    lastName: author.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.author),
-    isConfirmed: true,
-  },
-  recReviewer: {
-    type: 'user',
-    username: chance.word(),
-    email: recReviewer.email,
-    password: 'password',
-    admin: false,
-    id: recReviewer.id,
-    firstName: recReviewer.firstName,
-    lastName: recReviewer.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.recReviewer),
-    isConfirmed: true,
-    teams: [revTeamID],
-  },
-  handlingEditor: {
-    type: 'user',
-    username: chance.word(),
-    email: handlingEditor.email,
-    password: 'password',
-    admin: false,
-    id: handlingEditor.id,
-    firstName: handlingEditor.firstName,
-    lastName: handlingEditor.lastName,
-    teams: [heTeamID],
-    save: jest.fn(() => users.handlingEditor),
-    editorInChief: false,
-    handlingEditor: true,
-    title: 'Mr',
-  },
-  editorInChief: {
-    type: 'user',
-    username: chance.word(),
-    email: chance.email(),
-    password: 'password',
-    admin: false,
-    id: chance.guid(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.editorInChief),
-    isConfirmed: false,
-    editorInChief: true,
-  },
-}
-
-module.exports = users
diff --git a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..054d38e3feec67bb3dc33eafb4b4b5d430f4cfe5
--- /dev/null
+++ b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js
@@ -0,0 +1,133 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendNotificationEmail: jest.fn(),
+}))
+const reqBody = {}
+
+const path = '../routes/fragments/patch'
+const route = {
+  path: '/api/collections/:collectionId/fragments/:fragmentId/submit',
+}
+describe('Patch fragments route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the parameters are correct', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the fragmentId does not match the collectionId', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    collection.fragments.length = 0
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Collection and fragment do not match.')
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: 'invalid-id',
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Item not found')
+  })
+  it('should return an error when no HE recommendation exists', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+    const { collection } = testFixtures.collections
+    fragment.recommendations.length = 0
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      'No Handling Editor recommendation has been found.',
+    )
+  })
+  it('should return an error when the request user is not the owner', async () => {
+    const { author } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+    const { collection } = testFixtures.collections
+    fragment.recommendations.length = 0
+
+    const res = await requests.sendRequest({
+      body,
+      userId: author.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+})
diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
index 0e3c8013b981d741859bcf134f48db99e59f7e33..31755149c0d5f99a086d4360f32214953eac2765 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
@@ -1,12 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendNotificationEmail: jest.fn(),
 }))
@@ -29,7 +29,7 @@ const reqBody = {
   recommendationType: 'review',
 }
 
-const path = '../../routes/fragmentsRecommendations/patch'
+const path = '../routes/fragmentsRecommendations/patch'
 const route = {
   path:
     '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId',
@@ -132,19 +132,20 @@ describe('Patch fragments recommendations route handler', () => {
     expect(data.error).toEqual('Recommendation not found.')
   })
   it('should return an error when the request user is not a reviewer', async () => {
-    const { author } = testFixtures.users
+    const { user } = testFixtures.users
     const { collection } = testFixtures.collections
     const { fragment } = testFixtures.fragments
 
     const res = await requests.sendRequest({
       body,
-      userId: author.id,
+      userId: user.id,
       models,
       route,
       path,
       params: {
         collectionId: collection.id,
         fragmentId: fragment.id,
+        recommendationId: fragment.recommendations[0].id,
       },
     })
 
diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
index 364448eb2425e07b4f0df6ebcb9de904201d0489..c0c33bd5e78826f5f0ee2d02738b9430cb59b6f1 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
@@ -1,12 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 const chance = new Chance()
 jest.mock('pubsweet-component-mail-service', () => ({
   sendNotificationEmail: jest.fn(),
@@ -29,7 +29,7 @@ const reqBody = {
   recommendationType: 'review',
 }
 
-const path = '../../routes/fragmentsRecommendations/post'
+const path = '../routes/fragmentsRecommendations/post'
 const route = {
   path: '/api/collections/:collectionId/fragments/:fragmentId/recommendations',
 }
@@ -57,7 +57,7 @@ describe('Post fragments recommendations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('Recommendation type is required.')
   })
-  it('should return success when the parameters are correct', async () => {
+  it('should return success when creating a recommendation as a reviewer', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
     const { fragment } = testFixtures.fragments
@@ -78,6 +78,27 @@ describe('Post fragments recommendations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.userId).toEqual(reviewer.id)
   })
+  it('should return success when creating a recommendation as a HE', async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: handlingEditor.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+    expect(data.userId).toEqual(handlingEditor.id)
+  })
   it('should return an error when the fragmentId does not match the collectionId', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
diff --git a/packages/component-manuscript-manager/src/tests/helpers/Model.js b/packages/component-manuscript-manager/src/tests/helpers/Model.js
deleted file mode 100644
index 3e5a7364b9a7e907530d21ae9575644a69294dbc..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/helpers/Model.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// const fixtures = require('../fixtures/fixtures')
-
-const UserMock = require('../mocks/User')
-
-const notFoundError = new Error()
-notFoundError.name = 'NotFoundError'
-notFoundError.status = 404
-
-const build = fixtures => {
-  const models = {
-    User: {},
-    Collection: {
-      find: jest.fn(id => findMock(id, 'collections', fixtures)),
-    },
-    Fragment: {
-      find: jest.fn(id => findMock(id, 'fragments', fixtures)),
-    },
-    Team: {
-      find: jest.fn(id => findMock(id, 'teams', fixtures)),
-    },
-  }
-  UserMock.find = jest.fn(id => findMock(id, 'users', fixtures))
-  UserMock.all = jest.fn(() => Object.values(fixtures.users))
-  models.User = UserMock
-  return models
-}
-
-const findMock = (id, type, fixtures) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj.id === id,
-  )
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-module.exports = { build }
diff --git a/packages/component-manuscript-manager/src/tests/mocks/User.js b/packages/component-manuscript-manager/src/tests/mocks/User.js
deleted file mode 100644
index b337c5f31ce5d71eaadd61ccb95fb1f83eef7d83..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/mocks/User.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/* eslint-disable func-names-any */
-const uuid = require('uuid')
-
-function User(properties) {
-  this.type = 'user'
-  this.email = properties.email
-  this.username = properties.username
-  this.password = properties.password
-  this.roles = properties.roles
-  this.title = properties.title
-  this.affiliation = properties.affiliation
-  this.firstName = properties.firstName
-  this.lastName = properties.lastName
-  this.admin = properties.admin
-}
-
-User.prototype.save = jest.fn(function saveUser() {
-  this.id = uuid.v4()
-  return Promise.resolve(this)
-})
-
-module.exports = User
diff --git a/packages/component-manuscript-manager/src/tests/helpers/requests.js b/packages/component-manuscript-manager/src/tests/requests.js
similarity index 100%
rename from packages/component-manuscript-manager/src/tests/helpers/requests.js
rename to packages/component-manuscript-manager/src/tests/requests.js
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index df4b1eaac76cebaf609b4b5fe0d5768e25eb2979..9289bd24896b42155ba1ec14286b0beeaefe2911 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -53,7 +53,10 @@ export default compose(
       hasManuscriptFailure: hasManuscriptFailure(state),
       version: selectFragment(state, match.params.version),
       project: selectCollection(state, match.params.project),
-      editorialRecommendations: selectEditorialRecommendations(state),
+      editorialRecommendations: selectEditorialRecommendations(
+        state,
+        match.params.version,
+      ),
       canSeeEditorialComments: canSeeEditorialComments(
         state,
         match.params.project,
diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js
index b8ca662a4109bdd916cf195c0690bda6bc6e1c17..2da95ea14fcfeb5d219f7f47a57829a27facb1c6 100644
--- a/packages/component-manuscript/src/components/ReviewerReportForm.js
+++ b/packages/component-manuscript/src/components/ReviewerReportForm.js
@@ -25,7 +25,6 @@ import AutosaveIndicator from 'pubsweet-component-wizard/src/components/Autosave
 import {
   uploadFile,
   deleteFile,
-  getFileError,
   getSignedUrl,
   getRequestStatus,
 } from 'pubsweet-components-faraday/src/redux/files'
@@ -40,8 +39,6 @@ import {
 } from 'pubsweet-component-modal/src/components'
 
 import {
-  selectError,
-  selectFetching,
   createRecommendation,
   updateRecommendation,
 } from 'pubsweet-components-faraday/src/redux/recommendations'
@@ -62,7 +59,6 @@ const ReviewerReportForm = ({
   fileError,
   removeFile,
   changeField,
-  errorRequest,
   isSubmitting,
   handleSubmit,
   fileFetching,
@@ -172,11 +168,6 @@ const ReviewerReportForm = ({
         <ErrorText>{fileError}</ErrorText>
       </Row>
     )}
-    {errorRequest && (
-      <Row>
-        <ErrorText>{errorRequest}</ErrorText>
-      </Row>
-    )}
     <Row>
       <ActionButton onClick={handleSubmit}> Submit report </ActionButton>
       <AutosaveIndicator
@@ -189,8 +180,7 @@ const ReviewerReportForm = ({
 
 const ModalWrapper = compose(
   connect(state => ({
-    modalError: selectError(state),
-    fetching: selectFetching(state),
+    fetching: false,
   })),
 )(({ fetching, ...rest }) => (
   <ConfirmationModal {...rest} isFetching={fetching} />
@@ -200,8 +190,6 @@ export default compose(
   withJournal,
   connect(
     state => ({
-      fileError: getFileError(state),
-      errorRequest: selectError(state),
       fileFetching: getRequestStatus(state),
       formValues: getFormValues('reviewerReport')(state),
       isSubmitting: isSubmitting('reviewerReport')(state),
diff --git a/packages/component-manuscript/src/components/ReviewsAndReports.js b/packages/component-manuscript/src/components/ReviewsAndReports.js
index b498bf92a2eab8ad79f3071057a1adf539c29efd..ced7097c6c243e89b12228118c210f16fa3caedf 100644
--- a/packages/component-manuscript/src/components/ReviewsAndReports.js
+++ b/packages/component-manuscript/src/components/ReviewsAndReports.js
@@ -1,6 +1,6 @@
 import React, { Fragment } from 'react'
-import { head } from 'lodash'
 import { th } from '@pubsweet/ui'
+import { head, get } from 'lodash'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
 import { compose, withHandlers, lifecycle, withProps } from 'recompose'
@@ -51,6 +51,9 @@ const ReviewsAndReports = ({
   mappedReviewers,
   mappedRecommendations,
   canSeeReviewersReports,
+  //
+  reviewerRecommendation,
+  //
   review = {},
   reviewers = [],
   recommendations = [],
@@ -82,13 +85,13 @@ const ReviewsAndReports = ({
     {isReviewer && (
       <Root id="review-report">
         <Expandable label="Your Report" startExpanded>
-          {report ? (
-            <ReviewReportCard report={report} />
+          {get(reviewerRecommendation, 'submittedOn') ? (
+            <ReviewReportCard report={reviewerRecommendation} />
           ) : (
             <ReviewerReportForm
               modalKey={`review-${project.id}`}
               project={project}
-              review={review}
+              review={reviewerRecommendation}
               version={version}
             />
           )}
@@ -100,18 +103,18 @@ const ReviewsAndReports = ({
 
 export default compose(
   connect(
-    (state, { project }) => ({
+    (state, { project, version }) => ({
       reviewers: selectReviewers(state),
-      recommendations: selectRecommendations(state),
       fetchingReviewers: selectFetchingReviewers(state),
-      isReviewer: currentUserIsReviewer(state, project.id),
+      isReviewer: currentUserIsReviewer(state, version.id),
+      recommendations: selectRecommendations(state, version.id),
       canSeeReviewersReports: canSeeReviewersReports(state, project.id),
     }),
     { getCollectionReviewers },
   ),
   withHandlers({
-    getReviewers: ({ project, getCollectionReviewers }) => () => {
-      getCollectionReviewers(project.id)
+    getReviewers: ({ project, version, getCollectionReviewers }) => () => {
+      getCollectionReviewers(project.id, version.id)
     },
     mappedRecommendations: ({ recommendations, reviewers }) => () =>
       recommendations.filter(r => r.submittedOn).map(r => ({
@@ -127,6 +130,7 @@ export default compose(
   withProps(({ recommendations = [] }) => ({
     review: head(recommendations),
     report: head(recommendations.filter(r => r.submittedOn)),
+    reviewerRecommendation: head(recommendations),
   })),
   lifecycle({
     componentDidMount() {
diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js
index 01f60511fa7ccfb30a467976c768c876c80e4fe2..c85422badd0f82227aef0f2b234819eccdfbc0f2 100644
--- a/packages/component-manuscript/src/components/SideBarActions.js
+++ b/packages/component-manuscript/src/components/SideBarActions.js
@@ -2,13 +2,19 @@ import React from 'react'
 import { compose } from 'recompose'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
-import { th, Icon } from '@pubsweet/ui'
+import { th } from '@pubsweet/ui-toolkit'
+import { Icon, Button } from '@pubsweet/ui'
+import { withRouter } from 'react-router-dom'
 import ZipFiles from 'pubsweet-components-faraday/src/components/Files/ZipFiles'
 import {
   Decision,
   Recommendation,
 } from 'pubsweet-components-faraday/src/components'
+
+import { createRevision } from 'pubsweet-component-wizard/src/redux/conversion'
+
 import {
+  canMakeRevision,
   canMakeDecision,
   canMakeRecommendation,
 } from '../../../component-faraday-selectors'
@@ -16,10 +22,15 @@ import {
 const SideBarActions = ({
   project,
   version,
+  createRevision,
+  canMakeRevision,
   canMakeDecision,
   canMakeRecommendation,
 }) => (
   <Root>
+    {canMakeRevision && (
+      <DecisionButton onClick={createRevision}>Submit revision</DecisionButton>
+    )}
     {canMakeDecision && (
       <Decision
         collectionId={project.id}
@@ -48,10 +59,17 @@ const SideBarActions = ({
 )
 
 export default compose(
-  connect((state, { project }) => ({
-    canMakeDecision: canMakeDecision(state, project),
-    canMakeRecommendation: canMakeRecommendation(state, project),
-  })),
+  withRouter,
+  connect(
+    (state, { project }) => ({
+      canMakeDecision: canMakeDecision(state, project),
+      canMakeRevision: canMakeRevision(state, project),
+      canMakeRecommendation: canMakeRecommendation(state, project),
+    }),
+    (dispatch, { project, version, history }) => ({
+      createRevision: () => dispatch(createRevision(project, version, history)),
+    }),
+  ),
 )(SideBarActions)
 
 // #region styled-components
@@ -68,4 +86,17 @@ const ClickableIcon = styled.div`
     opacity: 0.7;
   }
 `
+
+const DecisionButton = styled(Button)`
+  align-items: center;
+  background-color: ${th('colorPrimary')};
+  color: ${th('colorTextReverse')};
+  display: flex;
+  font-family: ${th('fontReading')};
+  font-size: ${th('fontSizeBaseSmall')};
+  height: calc(${th('subGridUnit')} * 5);
+  padding: calc(${th('subGridUnit')} / 2) ${th('subGridUnit')};
+  text-align: center;
+  white-space: nowrap;
+`
 // #endregion
diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js
index d9785efb43eeaa1501c8f74b40d292c0d308a4c0..ed5e3885bbfad9c55e488500582ab05c8ed6ca66 100644
--- a/packages/component-manuscript/src/components/utils.js
+++ b/packages/component-manuscript/src/components/utils.js
@@ -1,6 +1,8 @@
 import moment from 'moment'
 import { get, find, capitalize, omit, isEmpty, isEqual, debounce } from 'lodash'
 
+import { actions } from 'pubsweet-client/src'
+import { change as changeForm } from 'redux-form'
 import {
   autosaveRequest,
   autosaveSuccess,
@@ -88,7 +90,7 @@ export const redirectToError = redirectFn => err => {
 }
 
 export const parseReviewResponseToForm = (review = {}) => {
-  if (isEmpty(review)) return null
+  if (isEmpty(review)) return {}
   const comments = review.comments || []
   const publicComment = comments.find(c => c.public)
   const privateComment = comments.find(c => !c.public)
@@ -102,7 +104,7 @@ export const parseReviewResponseToForm = (review = {}) => {
 }
 
 export const parseReviewRequest = (review = {}) => {
-  if (isEmpty(review)) return null
+  if (isEmpty(review)) return {}
   const comments = [
     {
       public: true,
@@ -139,7 +141,6 @@ const onChange = (
 ) => {
   const newValues = parseReviewRequest(values)
   const prevValues = parseReviewRequest(previousValues)
-
   if (!isEqual(newValues, prevValues)) {
     dispatch(autosaveRequest())
     if (newValues.id) {
@@ -148,7 +149,10 @@ const onChange = (
         .catch(e => dispatch(autosaveFailure(e)))
     } else {
       createRecommendation(project.id, version.id, newValues)
-        .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
+        .then(r => {
+          dispatch(changeForm('reviewerReport', 'id', r.id))
+          return dispatch(autosaveSuccess(get(r, 'updatedOn')))
+        })
         .catch(e => dispatch(autosaveFailure(e)))
     }
   }
@@ -178,7 +182,10 @@ export const onReviewSubmit = (
       dispatch(autosaveRequest())
       updateRecommendation(project.id, version.id, newValues)
         .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
-        .then(hideModal)
+        .then(() => {
+          dispatch(actions.getFragments())
+          hideModal()
+        })
     },
     onCancel: hideModal,
   })
diff --git a/packages/component-user-manager/config/authsome-helpers.js b/packages/component-user-manager/config/authsome-helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..55148df349bc9807c0d6121fe30593dd021d237b
--- /dev/null
+++ b/packages/component-user-manager/config/authsome-helpers.js
@@ -0,0 +1,131 @@
+const omit = require('lodash/omit')
+const config = require('config')
+const get = require('lodash/get')
+
+const statuses = config.get('statuses')
+
+const publicStatusesPermissions = ['author', 'reviewer']
+
+const parseAuthorsData = (coll, matchingCollPerm) => {
+  if (['reviewer'].includes(matchingCollPerm.permission)) {
+    coll.authors = coll.authors.map(a => omit(a, ['email']))
+  }
+}
+
+const setPublicStatuses = (coll, matchingCollPerm) => {
+  const status = get(coll, 'status') || 'draft'
+  // coll.visibleStatus = statuses[status].public
+  if (publicStatusesPermissions.includes(matchingCollPerm.permission)) {
+    coll.visibleStatus = statuses[status].public
+  }
+}
+
+const filterRefusedInvitations = (coll, user) => {
+  const matchingInv = coll.invitations.find(inv => inv.userId === user.id)
+  if (matchingInv === undefined) return null
+  if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null
+  return coll
+}
+
+const filterObjectData = (
+  collectionsPermissions = [],
+  object = {},
+  user = {},
+) => {
+  if (object.type === 'fragment') {
+    const matchingCollPerm = collectionsPermissions.find(
+      collPerm => object.id === collPerm.fragmentId,
+    )
+    if (matchingCollPerm === undefined) return null
+    if (['reviewer'].includes(matchingCollPerm.permission)) {
+      object.files = omit(object.files, ['coverLetter'])
+      if (object.recommendations)
+        object.recommendations = object.recommendations.filter(
+          rec => rec.userId === user.id,
+        )
+    }
+    parseAuthorsData(object, matchingCollPerm)
+    if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
+      return filterRefusedInvitations(object, user)
+    }
+    return object
+  }
+  const matchingCollPerm = collectionsPermissions.find(
+    collPerm => object.id === collPerm.id,
+  )
+  if (matchingCollPerm === undefined) return null
+  setPublicStatuses(object, matchingCollPerm)
+
+  return object
+}
+
+const getTeamsByPermissions = async (
+  teamIds = [],
+  permissions = [],
+  TeamModel,
+) =>
+  (await Promise.all(
+    teamIds.map(async teamId => {
+      const team = await TeamModel.find(teamId)
+      if (!permissions.includes(team.teamType.permissions)) {
+        return null
+      }
+      return team
+    }),
+  )).filter(Boolean)
+
+const heIsInvitedToFragment = async ({ user, Team, collectionId }) =>
+  (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some(
+    // user is a member of the team with access to the fragment's parent collection
+    t => t.members.includes(user.id) && t.object.id === collectionId,
+  )
+
+const getUserPermissions = async ({
+  user,
+  Team,
+  mapFn = t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }),
+}) =>
+  (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn)
+
+const isOwner = ({ user: { id }, object }) => {
+  if (object.owners.includes(id)) return true
+  return !!object.owners.find(own => own.id === id)
+}
+
+const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => {
+  const userPermissions = await getUserPermissions({
+    user,
+    Team,
+  })
+
+  return !!userPermissions.find(p => {
+    const hasObject =
+      p.objectId === get(object, 'fragment.id') ||
+      p.objectId === get(object, 'fragment.collectionId')
+    if (roles.length > 0) {
+      return hasObject && roles.includes(p.role)
+    }
+    return hasObject
+  })
+}
+
+const isHandlingEditor = ({ user, object }) =>
+  get(object, 'collection.handlingEditor.id') === user.id
+
+module.exports = {
+  filterObjectData,
+  parseAuthorsData,
+  setPublicStatuses,
+  getTeamsByPermissions,
+  filterRefusedInvitations,
+  //
+  isOwner,
+  isHandlingEditor,
+  getUserPermissions,
+  heIsInvitedToFragment,
+  hasPermissionForObject,
+}
diff --git a/packages/component-user-manager/config/authsome-mode.js b/packages/component-user-manager/config/authsome-mode.js
new file mode 100644
index 0000000000000000000000000000000000000000..20e0d691891e9804d44e7912a821e560b3ccdaf9
--- /dev/null
+++ b/packages/component-user-manager/config/authsome-mode.js
@@ -0,0 +1,253 @@
+const config = require('config')
+const { get, pickBy, omit } = require('lodash')
+
+const statuses = config.get('statuses')
+const helpers = require('./authsome-helpers')
+
+function unauthenticatedUser(operation, object) {
+  // Public/unauthenticated users can GET /collections, filtered by 'published'
+  if (operation === 'GET' && object && object.path === '/collections') {
+    return {
+      filter: collections =>
+        collections.filter(collection => collection.published),
+    }
+  }
+
+  // Public/unauthenticated users can GET /collections/:id/fragments, filtered by 'published'
+  if (
+    operation === 'GET' &&
+    object &&
+    object.path === '/collections/:id/fragments'
+  ) {
+    return {
+      filter: fragments => fragments.filter(fragment => fragment.published),
+    }
+  }
+
+  // and filtered individual collection's properties: id, title, source, content, owners
+  if (operation === 'GET' && object && object.type === 'collection') {
+    if (object.published) {
+      return {
+        filter: collection =>
+          pickBy(collection, (_, key) =>
+            ['id', 'title', 'owners'].includes(key),
+          ),
+      }
+    }
+  }
+
+  if (operation === 'GET' && object && object.type === 'fragment') {
+    if (object.published) {
+      return {
+        filter: fragment =>
+          pickBy(fragment, (_, key) =>
+            ['id', 'title', 'source', 'presentation', 'owners'].includes(key),
+          ),
+      }
+    }
+  }
+
+  return false
+}
+
+const publicStatusesPermissions = ['author', 'reviewer']
+const createPaths = ['/collections', '/collections/:collectionId/fragments']
+
+async function authenticatedUser(user, operation, object, context) {
+  if (operation === 'GET') {
+    if (get(object, 'path') === '/collections') {
+      return {
+        filter: async collections => {
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          return collections.filter(collection => {
+            if (collection.owners.includes(user.id)) {
+              return true
+            }
+            const collectionPermission = userPermissions.find(
+              p => p.objectId === collection.id,
+            )
+            if (collectionPermission) {
+              return true
+            }
+
+            const fragmentPermission = userPermissions.find(p =>
+              collection.fragments.includes(p.objectId),
+            )
+            if (fragmentPermission) {
+              return true
+            }
+            return false
+          })
+        },
+      }
+    }
+
+    if (object === '/users') {
+      return true
+    }
+
+    if (get(object, 'type') === 'collection') {
+      if (helpers.isOwner({ user, object })) {
+        return true
+      }
+      return {
+        filter: async collection => {
+          const status = get(collection, 'status') || 'draft'
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          if (collection.owners.map(o => o.id).includes(user.id)) {
+            return collection
+          }
+
+          const collectionPermission = userPermissions.find(
+            p => p.objectId === collection.id,
+          )
+          if (
+            publicStatusesPermissions.includes(
+              get(collectionPermission, 'role'),
+            )
+          ) {
+            collection.visibleStatus = statuses[status].public
+          }
+          return collection
+        },
+      }
+    }
+
+    if (get(object, 'type') === 'fragment') {
+      if (helpers.isOwner({ user, object })) {
+        return true
+      }
+
+      const userPermissions = await helpers.getUserPermissions({
+        user,
+        Team: context.models.Team,
+      })
+
+      const permission = userPermissions.find(
+        p => p.objectId === object.id || p.objectId === object.collectionId,
+      )
+
+      if (!permission) return false
+
+      return {
+        filter: fragment => {
+          // handle other roles
+          if (permission.role === 'reviewer') {
+            fragment.files = omit(fragment.files, ['coverLetter'])
+            fragment.authors = fragment.authors.map(a => omit(a, ['email']))
+          }
+          return fragment
+        },
+      }
+    }
+
+    // allow HE to get reviewer invitations
+    if (get(object, 'fragment.type') === 'fragment') {
+      const collectionId = get(object, 'fragment.collectionId')
+      const collection = await context.models.Collection.find(collectionId)
+
+      if (get(collection, 'handlingEditor.id') === user.id) {
+        return true
+      }
+    }
+
+    if (get(object, 'type') === 'user') {
+      return true
+    }
+  }
+
+  if (operation === 'POST') {
+    // allow everytone to create manuscripts and versions
+    if (createPaths.includes(object.path)) {
+      return true
+    }
+
+    // allow HE to invite
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
+
+    // allow HE or assigned reviewers to recommend
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer', 'handlingEditor'],
+      })
+    }
+  }
+
+  if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
+    }
+
+    if (get(object, 'type') === 'fragment') {
+      return helpers.isOwner({ user, object })
+    }
+
+    // allow reviewer to patch his recommendation
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer'],
+      })
+    }
+
+    if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
+      return true
+    }
+  }
+
+  if (operation === 'DELETE') {
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
+  }
+
+  // If no individual permissions exist (above), fallback to unauthenticated
+  // user's permission
+  return unauthenticatedUser(operation, object)
+}
+
+const authsomeMode = async (userId, operation, object, context) => {
+  if (!userId) {
+    return unauthenticatedUser(operation, object)
+  }
+
+  // It's up to us to retrieve the relevant models for our
+  // authorization/authsome mode, e.g.
+  const user = await context.models.User.find(userId)
+
+  // Admins and editor in chiefs can do anything
+  if (user && (user.admin || user.editorInChief)) return true
+
+  if (user) {
+    return authenticatedUser(user, operation, object, context)
+  }
+
+  return false
+}
+
+module.exports = authsomeMode
diff --git a/packages/component-user-manager/config/default.js b/packages/component-user-manager/config/default.js
index 350b9de7761a91153fff07c37b29603005d36c33..5d8cb0c55cde6a7eeb0497b00fd12c5ed5751b1a 100644
--- a/packages/component-user-manager/config/default.js
+++ b/packages/component-user-manager/config/default.js
@@ -1,4 +1,17 @@
+const path = require('path')
+
 module.exports = {
+  authsome: {
+    mode: path.resolve(__dirname, 'authsome-mode.js'),
+    teams: {
+      handlingEditor: {
+        name: 'Handling Editors',
+      },
+      reviewer: {
+        name: 'Reviewer',
+      },
+    },
+  },
   mailer: {
     from: 'test@example.com',
   },
@@ -16,4 +29,62 @@ module.exports = {
       handlingEditor: ['reviewer'],
     },
   },
+  statuses: {
+    draft: {
+      public: 'Draft',
+      private: 'Draft',
+    },
+    submitted: {
+      public: 'Submitted',
+      private: 'Submitted',
+    },
+    heInvited: {
+      public: 'Submitted',
+      private: 'Handling Editor Invited',
+    },
+    heAssigned: {
+      public: 'Handling Editor Assigned',
+      private: 'Handling Editor Assigned',
+    },
+    reviewersInvited: {
+      public: 'Reviewers Invited',
+      private: 'Reviewers Invited',
+    },
+    underReview: {
+      public: 'Under Review',
+      private: 'Under Review',
+    },
+    reviewCompleted: {
+      public: 'Under Review',
+      private: 'Review Completed',
+    },
+    pendingApproval: {
+      public: 'Under Review',
+      private: 'Pending Approval',
+    },
+    revisionRequested: {
+      public: 'Revision Requested',
+      private: 'Revision Requested',
+    },
+    rejected: {
+      public: 'Rejected',
+      private: 'Rejected',
+    },
+    published: {
+      public: 'Published',
+      private: 'Published',
+    },
+  },
+  'manuscript-types': {
+    research: 'Research',
+    review: 'Review',
+    'clinical-study': 'Clinical Study',
+    'case-report': 'Case Report',
+    'letter-to-editor': 'Letter to the Editor',
+    editorial: 'Editorial',
+    corrigendum: 'Corrigendum',
+    erratum: 'Erratum',
+    'expression-of-concern': 'Expression of Concern',
+    retraction: 'Retraction',
+  },
 }
diff --git a/packages/component-user-manager/config/test.js b/packages/component-user-manager/config/test.js
index a1e52fc0b730d1b5e7836ac08eb6b0188b3c13ae..63452331a3b061996e51151df75e7d647efa5ed7 100644
--- a/packages/component-user-manager/config/test.js
+++ b/packages/component-user-manager/config/test.js
@@ -17,4 +17,62 @@ module.exports = {
       author: ['author'],
     },
   },
+  statuses: {
+    draft: {
+      public: 'Draft',
+      private: 'Draft',
+    },
+    submitted: {
+      public: 'Submitted',
+      private: 'Submitted',
+    },
+    heInvited: {
+      public: 'Submitted',
+      private: 'Handling Editor Invited',
+    },
+    heAssigned: {
+      public: 'Handling Editor Assigned',
+      private: 'Handling Editor Assigned',
+    },
+    reviewersInvited: {
+      public: 'Reviewers Invited',
+      private: 'Reviewers Invited',
+    },
+    underReview: {
+      public: 'Under Review',
+      private: 'Under Review',
+    },
+    reviewCompleted: {
+      public: 'Under Review',
+      private: 'Review Completed',
+    },
+    pendingApproval: {
+      public: 'Under Review',
+      private: 'Pending Approval',
+    },
+    revisionRequested: {
+      public: 'Revision Requested',
+      private: 'Revision Requested',
+    },
+    rejected: {
+      public: 'Rejected',
+      private: 'Rejected',
+    },
+    published: {
+      public: 'Published',
+      private: 'Published',
+    },
+  },
+  'manuscript-types': {
+    research: 'Research',
+    review: 'Review',
+    'clinical-study': 'Clinical Study',
+    'case-report': 'Case Report',
+    'letter-to-editor': 'Letter to the Editor',
+    editorial: 'Editorial',
+    corrigendum: 'Corrigendum',
+    erratum: 'Erratum',
+    'expression-of-concern': 'Expression of Concern',
+    retraction: 'Retraction',
+  },
 }
diff --git a/packages/component-user-manager/index.js b/packages/component-user-manager/index.js
index 4ba897be673285e0582abfcc2c2fabd03cc58373..86e7a8ab8fe92e4398708bcf9f077fe5cd9e804e 100644
--- a/packages/component-user-manager/index.js
+++ b/packages/component-user-manager/index.js
@@ -1,6 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/Users')(app)
-    require('./src/CollectionsUsers')(app)
+    require('./src/FragmentsUsers')(app)
   },
 }
diff --git a/packages/component-user-manager/src/CollectionsUsers.js b/packages/component-user-manager/src/FragmentsUsers.js
similarity index 51%
rename from packages/component-user-manager/src/CollectionsUsers.js
rename to packages/component-user-manager/src/FragmentsUsers.js
index 5ee6f750ff770c0b83fbaad57537206c54aa82f1..443397a6f81cb1d10c9696fb5a9d937becc2b1cb 100644
--- a/packages/component-user-manager/src/CollectionsUsers.js
+++ b/packages/component-user-manager/src/FragmentsUsers.js
@@ -1,16 +1,17 @@
 const bodyParser = require('body-parser')
 
-const CollectionsUsers = app => {
+const FragmentsUsers = app => {
   app.use(bodyParser.json())
-  const basePath = '/api/collections/:collectionId/users'
-  const routePath = './routes/collectionsUsers'
+  const basePath = '/api/collections/:collectionId/fragments/:fragmentId/users'
+  const routePath = './routes/fragmentsUsers'
   const authBearer = app.locals.passport.authenticate('bearer', {
     session: false,
   })
   /**
-   * @api {post} /api/collections/:collectionId/users Add a user to a collection
-   * @apiGroup CollectionsUsers
+   * @api {post} /api/collections/:collectionId/fragments/:fragmentId/users Add a user to a fragment
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiParamExample {json} Body
    *    {
    *      "email": "email@example.com",
@@ -22,19 +23,12 @@ const CollectionsUsers = app => {
    *    HTTP/1.1 200 OK
    *    {
    *      "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
-   *      "type": "user",
-   *      "admin": false,
    *      "email": "email@example.com",
-   *      "teams": [
-   *        "c576695a-7cda-4e27-8e9c-31f3a0e9d592"
-   *      ],
-   *      "username": "email@example.com",
-   *      "fragments": [],
-   *      "collections": [],
-   *      "isConfirmed": false,
-   *      "editorInChief": false,
-   *      "handlingEditor": false,
-   *      "passwordResetToken": "04590a2b7f6c1f37cb84881d529e516fa6fc309c205a07f1341b2bfaa6f2b46c"
+   *      "firstName": "John",
+   *      "lastName": "Smith",
+   *      "affiliation": "MIT",
+   *      "isSubmitting": true,
+   *      "isCorresponding": false
    *    }
    * @apiErrorExample {json} Invite user errors
    *    HTTP/1.1 400 Bad Request
@@ -46,9 +40,10 @@ const CollectionsUsers = app => {
     require(`${routePath}/post`)(app.locals.models),
   )
   /**
-   * @api {delete} /api/collections/:collectionId/user/:userId Delete user from collection
-   * @apiGroup CollectionsUsers
+   * @api {delete} /api/collections/:collectionId/fragments/:fragmentId/user/:userId Delete a user from a fragment
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiParam {userId} userId User id
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 {}
@@ -62,9 +57,10 @@ const CollectionsUsers = app => {
     require(`${routePath}/delete`)(app.locals.models),
   )
   /**
-   * @api {get} /api/collections/:collectionId/users List collections users
-   * @apiGroup CollectionsUsers
+   * @api {get} /api/collections/:collectionId/fragments/:fragmentId/users List fragment users
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 OK
    *    [{
@@ -107,62 +103,6 @@ const CollectionsUsers = app => {
    *    HTTP/1.1 404 Not Found
    */
   app.get(basePath, authBearer, require(`${routePath}/get`)(app.locals.models))
-  /**
-   * @api {patch} /api/collections/:collectionId/users/:userId Update a user on a collection
-   * @apiGroup CollectionsUsers
-   * @apiParam {collectionId} collectionId Collection id
-   * @apiParam {userId} userId User id
-   * @apiParamExample {json} Body
-   *    {
-   *      "isSubmitting": false,
-   *      "isCorresponding": true,
-   *      "firstName": "John",
-   *      "lastName": "Smith",
-   *      "affiliation": "UCLA"
-   *    }
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    {
-   *      "id": "7e8a77f9-8e5c-4fa3-b717-8df9932df128",
-   *      "type": "collection",
-   *      "owners": [
-   *        {
-   *           "id": "69ac1ee9-08a8-4ee6-a57c-c6c8be8d3c4f",
-   *           "username": "admin"
-   *        }
-   *      ],
-   *      "authors": [
-   *        {
-   *          "userId": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
-   *          "isSubmitting": false,
-   *          "isCorresponding": true
-   *        }
-   *      ],
-   *      "created": 1522829424474,
-   *      "customId": "9424466",
-   *      "fragments": [
-   *        "c35d0bd8-be03-4c16-b869-bd69796c5a21"
-   *      ],
-   *      "invitations": [
-   *        {
-   *          "id": "9043a836-0d49-4b8d-be0b-df39071b5c57",
-   *          "hasAnswer": false,
-   *          "timestamp": 1522831123430,
-   *          "isAccepted": false
-   *        },
-   *      ]
-   *    }
-   * @apiErrorExample {json} Update invitations errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   *    HTTP/1.1 500 Internal Server Error
-   */
-  app.patch(
-    `${basePath}/:userId`,
-    authBearer,
-    require(`${routePath}/patch`)(app.locals.models),
-  )
 }
 
-module.exports = CollectionsUsers
+module.exports = FragmentsUsers
diff --git a/packages/component-user-manager/src/helpers/Collection.js b/packages/component-user-manager/src/helpers/Collection.js
deleted file mode 100644
index c2079945210b19e05c5db988777b7e29d22c70b5..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/Collection.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-
-module.exports = {
-  addAuthor: async (
-    collection,
-    user,
-    res,
-    url,
-    isSubmitting,
-    isCorresponding,
-  ) => {
-    collection.authors = collection.authors || []
-    const author = {
-      userId: user.id,
-      firstName: user.firstName || '',
-      lastName: user.lastName || '',
-      email: user.email,
-      title: user.title || '',
-      affiliation: user.affiliation || '',
-      isSubmitting,
-      isCorresponding,
-    }
-    collection.authors.push(author)
-    await collection.save()
-    if (collection.owners.includes(user.id)) {
-      return res.status(200).json(user)
-    }
-    try {
-      mailService.sendSimpleEmail({
-        toEmail: user.email,
-        user,
-        emailType: 'add-author',
-        dashboardUrl: url,
-      })
-
-      return res.status(200).json(user)
-    } catch (e) {
-      logger.error(e)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
-  },
-}
diff --git a/packages/component-user-manager/src/helpers/Team.js b/packages/component-user-manager/src/helpers/Team.js
deleted file mode 100644
index 3b700c4436f1306a7b41b07ab4b9ded4aa566944..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/Team.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const logger = require('@pubsweet/logger')
-// const get = require('lodash/get')
-
-const createNewTeam = async (collectionId, role, userId, TeamModel) => {
-  let permissions, group, name
-  switch (role) {
-    case 'handlingEditor':
-      permissions = 'handlingEditor'
-      group = 'handlingEditor'
-      name = 'Handling Editor'
-      break
-    case 'reviewer':
-      permissions = 'reviewer'
-      group = 'reviewer'
-      name = 'Reviewer'
-      break
-    case 'author':
-      permissions = 'author'
-      group = 'author'
-      name = 'author'
-      break
-    default:
-      break
-  }
-
-  const teamBody = {
-    teamType: {
-      name: role,
-      permissions,
-    },
-    group,
-    name,
-    object: {
-      type: 'collection',
-      id: collectionId,
-    },
-    members: [userId],
-  }
-  let team = new TeamModel(teamBody)
-  team = await team.save()
-  return team
-}
-
-const setupManuscriptTeam = async (models, user, collectionId, role) => {
-  const teams = await models.Team.all()
-  user.teams = user.teams || []
-  const filteredTeams = teams.filter(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-
-  if (filteredTeams.length > 0) {
-    let team = filteredTeams[0]
-    team.members.push(user.id)
-
-    try {
-      team = await team.save()
-      user.teams.push(team.id)
-      await user.save()
-      return team
-    } catch (e) {
-      logger.error(e)
-    }
-  } else {
-    const team = await createNewTeam(collectionId, role, user.id, models.Team)
-    user.teams.push(team.id)
-    await user.save()
-    return team
-  }
-}
-
-const removeTeamMember = async (teamId, userId, TeamModel) => {
-  const team = await TeamModel.find(teamId)
-  const members = team.members.filter(member => member !== userId)
-  team.members = members
-
-  await team.save()
-}
-
-const getTeamMembersByCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  // const members = get(
-  //   teams.find(
-  //     team =>
-  //       team.group === role &&
-  //       team.object.type === 'collection' &&
-  //       team.object.id === collectionId,
-  //   ),
-  //   'members',
-  // )
-  const team = teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-
-  return team.members
-}
-
-const getTeamByGroupAndCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  return teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-}
-
-module.exports = {
-  createNewTeam,
-  setupManuscriptTeam,
-  removeTeamMember,
-  getTeamMembersByCollection,
-  getTeamByGroupAndCollection,
-}
diff --git a/packages/component-user-manager/src/helpers/User.js b/packages/component-user-manager/src/helpers/User.js
deleted file mode 100644
index 41077ba17fb36fd8cf22f62998901bccd630ec58..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/User.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const helpers = require('./helpers')
-const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-
-module.exports = {
-  setupNewUser: async (
-    body,
-    url,
-    res,
-    email,
-    role,
-    UserModel,
-    invitationType,
-  ) => {
-    const { firstName, lastName, affiliation, title } = body
-    const newUser = await helpers.createNewUser(
-      email,
-      firstName,
-      lastName,
-      affiliation,
-      title,
-      UserModel,
-      role,
-    )
-
-    try {
-      mailService.sendSimpleEmail({
-        toEmail: newUser.email,
-        user: newUser,
-        emailType: invitationType,
-        dashboardUrl: url,
-      })
-
-      return newUser
-    } catch (e) {
-      logger.error(e.message)
-      return { status: 500, error: 'Email could not be sent.' }
-    }
-  },
-}
diff --git a/packages/component-user-manager/src/helpers/helpers.js b/packages/component-user-manager/src/helpers/helpers.js
deleted file mode 100644
index 0628217ee67a8b0d6697869a7cf7beb491dfc8af..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/helpers.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const logger = require('@pubsweet/logger')
-const uuid = require('uuid')
-const crypto = require('crypto')
-
-const checkForUndefinedParams = (...params) => {
-  if (params.includes(undefined)) {
-    return false
-  }
-
-  return true
-}
-
-const validateEmailAndToken = async (email, token, userModel) => {
-  try {
-    const user = await userModel.findByEmail(email)
-    if (user) {
-      if (token !== user.passwordResetToken) {
-        logger.error(
-          `invite pw reset tokens do not match: REQ ${token} vs. DB ${
-            user.passwordResetToken
-          }`,
-        )
-        return {
-          success: false,
-          status: 400,
-          message: 'invalid request',
-        }
-      }
-      return { success: true, user }
-    }
-  } catch (e) {
-    if (e.name === 'NotFoundError') {
-      logger.error('invite pw reset on non-existing user')
-      return {
-        success: false,
-        status: 404,
-        message: 'user not found',
-      }
-    } else if (e.name === 'ValidationError') {
-      logger.error('invite pw reset validation error')
-      return {
-        success: false,
-        status: 400,
-        message: e.details[0].message,
-      }
-    }
-    logger.error('internal server error')
-    return {
-      success: false,
-      status: 500,
-      message: e.details[0].message,
-    }
-  }
-  return {
-    success: false,
-    status: 500,
-    message: 'something went wrong',
-  }
-}
-
-const handleNotFoundError = async (error, item) => {
-  const response = {
-    success: false,
-    status: 500,
-    message: 'Something went wrong',
-  }
-  if (error.name === 'NotFoundError') {
-    logger.error(`invalid ${item} id`)
-    response.status = 404
-    response.message = `${item} not found`
-    return response
-  }
-
-  logger.error(error)
-  return response
-}
-
-const createNewUser = async (
-  email,
-  firstName,
-  lastName,
-  affiliation,
-  title,
-  UserModel,
-  role,
-) => {
-  const username = email
-  const password = uuid.v4()
-  const userBody = {
-    username,
-    email,
-    password,
-    passwordResetToken: crypto.randomBytes(32).toString('hex'),
-    isConfirmed: false,
-    firstName,
-    lastName,
-    affiliation,
-    title,
-    editorInChief: role === 'editorInChief',
-    admin: role === 'admin',
-    handlingEditor: role === 'handlingEditor',
-  }
-
-  let newUser = new UserModel(userBody)
-
-  try {
-    newUser = await newUser.save()
-    return newUser
-  } catch (e) {
-    logger.error(e)
-  }
-}
-
-module.exports = {
-  checkForUndefinedParams,
-  validateEmailAndToken,
-  handleNotFoundError,
-  createNewUser,
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/delete.js b/packages/component-user-manager/src/routes/collectionsUsers/delete.js
deleted file mode 100644
index 2fae5bea551fdb6795c0fa0224a0d9f935f59e68..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/delete.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
-
-module.exports = models => async (req, res) => {
-  // TO DO: handle route  access with authsome
-  const { collectionId, userId } = req.params
-  try {
-    const collection = await models.Collection.find(collectionId)
-    const user = await models.User.find(userId)
-
-    const team = await teamHelper.getTeamByGroupAndCollection(
-      collectionId,
-      'author',
-      models.Team,
-    )
-
-    collection.authors = collection.authors.filter(
-      author => author.userId !== userId,
-    )
-    await collection.save()
-    await teamHelper.removeTeamMember(team.id, userId, models.Team)
-    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
-    delete user.passwordResetToken
-    await user.save()
-    return res.status(200).json({})
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'item')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/get.js b/packages/component-user-manager/src/routes/collectionsUsers/get.js
deleted file mode 100644
index 1d6b395dd79dfeaa3e5b38f8c00a4b29d6c5716b..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/get.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
-
-module.exports = models => async (req, res) => {
-  // TO DO: add authsome
-  const { collectionId } = req.params
-  try {
-    const collection = await models.Collection.find(collectionId)
-    if (collection.authors === undefined) {
-      return res.status(200).json([])
-    }
-    const members = await teamHelper.getTeamMembersByCollection(
-      collectionId,
-      'author',
-      models.Team,
-    )
-
-    if (members === undefined) {
-      res.status(400).json({
-        error: 'The requested collection does not have an author Team',
-      })
-      return
-    }
-    const membersData = members.map(async member => {
-      const user = await models.User.find(member)
-      const matchingAuthor = collection.authors.find(
-        author => author.userId === user.id,
-      )
-      return {
-        ...user,
-        isSubmitting: matchingAuthor.isSubmitting,
-        isCorresponding: matchingAuthor.isCorresponding,
-      }
-    })
-
-    const resBody = await Promise.all(membersData)
-    res.status(200).json(resBody)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/patch.js b/packages/component-user-manager/src/routes/collectionsUsers/patch.js
deleted file mode 100644
index c973e20d44faaae59eab82afd22a5399e2321924..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/patch.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const logger = require('@pubsweet/logger')
-const helpers = require('../../helpers/helpers')
-
-module.exports = models => async (req, res) => {
-  // TO DO: add authsome
-  const { collectionId, userId } = req.params
-  const {
-    isSubmitting,
-    isCorresponding,
-    firstName,
-    lastName,
-    affiliation,
-  } = req.body
-
-  if (!helpers.checkForUndefinedParams(isSubmitting, isCorresponding)) {
-    res.status(400).json({ error: 'Missing parameters' })
-    logger.error('some parameters are missing')
-    return
-  }
-
-  try {
-    let collection = await models.Collection.find(collectionId)
-    if (collection.authors === undefined) {
-      return res.status(400).json({
-        error: 'Collection does not have any authors',
-      })
-    }
-    const user = await models.User.find(userId)
-    const matchingAuthor = collection.authors.find(
-      author => author.userId === user.id,
-    )
-    if (matchingAuthor === undefined) {
-      return res.status(400).json({
-        error: 'Collection and user do not match',
-      })
-    }
-    user.firstName = firstName
-    user.lastName = lastName
-    user.affiliation = affiliation
-    await user.save()
-    matchingAuthor.isSubmitting = isSubmitting
-    matchingAuthor.isCorresponding = isCorresponding
-    collection = await collection.save()
-    res.status(200).json(collection)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'item')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/post.js b/packages/component-user-manager/src/routes/collectionsUsers/post.js
deleted file mode 100644
index 73957c570bea10d9dc4e1e691690e6e8170916ca..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/post.js
+++ /dev/null
@@ -1,91 +0,0 @@
-const get = require('lodash/get')
-const helpers = require('../../helpers/helpers')
-const collectionHelper = require('../../helpers/Collection')
-const teamHelper = require('../../helpers/Team')
-const userHelper = require('../../helpers/User')
-
-module.exports = models => async (req, res) => {
-  const { email, role, isSubmitting, isCorresponding } = req.body
-
-  if (
-    !helpers.checkForUndefinedParams(email, role, isSubmitting, isCorresponding)
-  )
-    return res.status(400).json({ error: 'Missing parameters.' })
-
-  const collectionId = get(req, 'params.collectionId')
-  let collection
-  try {
-    collection = await models.Collection.find(collectionId)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-  const url = `${req.protocol}://${req.get('host')}`
-
-  try {
-    let user = await models.User.findByEmail(email)
-
-    if (role === 'author') {
-      await teamHelper.setupManuscriptTeam(models, user, collectionId, role)
-      // get updated user from DB
-      user = await models.User.find(user.id)
-      if (collection.authors !== undefined) {
-        const match = collection.authors.find(
-          author => author.userId === user.id,
-        )
-        if (match) {
-          return res.status(400).json({
-            error: `User ${user.email} is already an author`,
-          })
-        }
-      }
-      return await collectionHelper.addAuthor(
-        collection,
-        user,
-        res,
-        url,
-        isSubmitting,
-        isCorresponding,
-      )
-    }
-    return res.status(400).json({
-      error: `${role} is not defined`,
-    })
-  } catch (e) {
-    if (role !== 'author') {
-      return res.status(400).json({
-        error: `${role} is not defined`,
-      })
-    }
-    if (e.name === 'NotFoundError') {
-      const newUser = await userHelper.setupNewUser(
-        req.body,
-        url,
-        res,
-        email,
-        role,
-        models.User,
-        'invite-author',
-      )
-      if (newUser.error !== undefined) {
-        return res.status(newUser.status).json({
-          error: newUser.message,
-        })
-      }
-      await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role)
-      return collectionHelper.addAuthor(
-        collection,
-        newUser,
-        res,
-        url,
-        isSubmitting,
-        isCorresponding,
-      )
-    }
-    return res.status(e.status).json({
-      error: 'Something went wrong',
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/delete.js b/packages/component-user-manager/src/routes/fragmentsUsers/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..86073f5d5d467b23878dc792c088a86877c28e88
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/delete.js
@@ -0,0 +1,31 @@
+const { Team, services } = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  // TO DO: handle route  access with authsome
+  const { collectionId, userId, fragmentId } = req.params
+  try {
+    const collection = await models.Collection.find(collectionId)
+    const user = await models.User.find(userId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    const teamHelper = new Team({ TeamModel: models.Team, fragmentId })
+
+    const team = await teamHelper.getTeam({
+      role: 'author',
+      objectType: 'fragment',
+    })
+
+    await teamHelper.removeTeamMember({ teamId: team.id, userId })
+    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
+    delete user.passwordResetToken
+    await user.save()
+    return res.status(200).json({})
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/get.js b/packages/component-user-manager/src/routes/fragmentsUsers/get.js
new file mode 100644
index 0000000000000000000000000000000000000000..76ef6041615e728b141e04f537ae6b67e643dd86
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/get.js
@@ -0,0 +1,21 @@
+const { services } = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  // TO DO: add authsome
+  const { collectionId, fragmentId } = req.params
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+
+    const { authors = [] } = await models.Fragment.find(fragmentId)
+    return res.status(200).json(authors)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d9b8a4ce8475e3658f5632cc4e9267e02a3198f
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js
@@ -0,0 +1,137 @@
+const { pick } = require('lodash')
+const mailService = require('pubsweet-component-mail-service')
+
+const {
+  User,
+  Team,
+  services,
+  Fragment,
+} = require('pubsweet-component-helper-service')
+
+const authorKeys = [
+  'id',
+  'email',
+  'title',
+  'lastName',
+  'firstName',
+  'affiliation',
+]
+
+// TODO: add authsome
+module.exports = models => async (req, res) => {
+  const { email, role, isSubmitting, isCorresponding } = req.body
+
+  if (
+    !services.checkForUndefinedParams(
+      email,
+      role,
+      isSubmitting,
+      isCorresponding,
+    )
+  )
+    return res.status(400).json({ error: 'Missing parameters.' })
+
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+  const baseUrl = services.getBaseUrl(req)
+  const UserModel = models.User
+  const teamHelper = new Team({ TeamModel: models.Team, fragmentId })
+  const fragmentHelper = new Fragment({ fragment })
+
+  try {
+    let user = await UserModel.findByEmail(email)
+
+    if (role !== 'author') {
+      return res.status(400).json({
+        error: `${role} is not defined`,
+      })
+    }
+
+    await teamHelper.setupTeam({ user, role, objectType: 'fragment' })
+    user = await UserModel.find(user.id)
+
+    fragment.authors = fragment.authors || []
+    const match = fragment.authors.find(author => author.id === user.id)
+
+    if (match) {
+      return res.status(400).json({
+        error: `User ${user.email} is already an author`,
+      })
+    }
+
+    await fragmentHelper.addAuthor({
+      user,
+      isSubmitting,
+      isCorresponding,
+    })
+
+    return res.status(200).json({
+      ...pick(user, authorKeys),
+      isSubmitting,
+      isCorresponding,
+    })
+  } catch (e) {
+    if (role !== 'author')
+      return res.status(400).json({
+        error: `${role} is not defined`,
+      })
+
+    if (e.name === 'NotFoundError') {
+      const userHelper = new User({ UserModel })
+      const newUser = await userHelper.setupNewUser({
+        url: baseUrl,
+        role,
+        invitationType: 'invite-author',
+        body: req.body,
+      })
+
+      if (newUser.error !== undefined)
+        return res.status(newUser.status).json({
+          error: newUser.message,
+        })
+
+      await teamHelper.setupTeam({
+        user: newUser,
+        role,
+        objectType: 'fragment',
+      })
+
+      await fragmentHelper.addAuthor({
+        user: newUser,
+        isSubmitting,
+        isCorresponding,
+      })
+
+      if (!collection.owners.includes(newUser.id)) {
+        mailService.sendSimpleEmail({
+          toEmail: newUser.email,
+          user: newUser,
+          emailType: 'add-author',
+          dashboardUrl: baseUrl,
+        })
+      }
+
+      return res.status(200).json({
+        ...pick(newUser, authorKeys),
+        isSubmitting,
+        isCorresponding,
+      })
+    }
+    return res.status(e.status).json({
+      error: `Something went wrong: ${e.name}`,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/users/resetPassword.js b/packages/component-user-manager/src/routes/users/resetPassword.js
index 6b6dc173e2db18ac9bd666505cda16c7042cb9fd..8a38aea627e1f0155ca26de947d80d2d6b7e375b 100644
--- a/packages/component-user-manager/src/routes/users/resetPassword.js
+++ b/packages/component-user-manager/src/routes/users/resetPassword.js
@@ -1,8 +1,8 @@
-const helpers = require('../../helpers/helpers')
+const { services } = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { email, password, token } = req.body
-  if (!helpers.checkForUndefinedParams(email, password, token))
+  if (!services.checkForUndefinedParams(email, password, token))
     return res.status(400).json({ error: 'missing required params' })
 
   if (password.length < 7)
@@ -10,11 +10,11 @@ module.exports = models => async (req, res) => {
       .status(400)
       .json({ error: 'password needs to be at least 7 characters long' })
 
-  const validateResponse = await helpers.validateEmailAndToken(
+  const validateResponse = await services.validateEmailAndToken({
     email,
     token,
-    models.User,
-  )
+    userModel: models.User,
+  })
 
   if (validateResponse.success === false)
     return res
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js b/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js
deleted file mode 100644
index 7242ac5c76c8b75ce0e265ca9ef98981490fe1a0..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js
+++ /dev/null
@@ -1,52 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-
-const models = Model.build()
-
-const { author, submittingAuthor } = fixtures.users
-const { standardCollection } = fixtures.collections
-const { authorTeam } = fixtures.teams
-const deletePath = '../../routes/collectionsUsers/delete'
-
-describe('Delete collections users route handler', () => {
-  it('should return success when an author is deleted', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-    expect(authorTeam.members).not.toContain(author.id)
-    expect(author.teams).not.toContain(authorTeam.id)
-  })
-  it('should return an error when the collection does not exist', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = 'invalid-id'
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the user does not exist', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = 'invalid-id'
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/get.test.js b/packages/component-user-manager/src/tests/collectionsUsers/get.test.js
deleted file mode 100644
index b5975b4ed8d941cd760c22b4df9ec729c41e8ab7..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/get.test.js
+++ /dev/null
@@ -1,38 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-
-const { standardCollection } = fixtures.collections
-const { submittingAuthor } = fixtures.users
-
-const getPath = '../../routes/collectionsUsers/get'
-describe('Get collections users route handler', () => {
-  it('should return success when the request data is correct', async () => {
-    const req = httpMocks.createRequest()
-    req.params.collectionId = standardCollection.id
-    req.user = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    const models = Model.build()
-    await require(getPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-    const data = JSON.parse(res._getData())
-    expect(data).toHaveLength(1)
-    expect(data[0].isSubmitting).toBeDefined()
-    expect(data[0].type).toBe('user')
-  })
-  it('should return an error when the collection does not exist', async () => {
-    const req = httpMocks.createRequest()
-    req.params.collectionId = 'invalid-id'
-    req.user = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    const models = Model.build()
-    await require(getPath)(models)(req, res)
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js b/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js
deleted file mode 100644
index 03c35f392d843bcf12c2b4859c656017a311c0a7..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js
+++ /dev/null
@@ -1,119 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-const Chance = require('chance')
-
-const chance = new Chance()
-
-const models = Model.build()
-jest.mock('pubsweet-component-mail-service', () => ({
-  sendSimpleEmail: jest.fn(),
-  sendNotificationEmail: jest.fn(),
-}))
-
-const { author, submittingAuthor } = fixtures.users
-const { standardCollection, authorsCollection } = fixtures.collections
-const body = {
-  isSubmitting: false,
-  isCorresponding: true,
-  firstName: chance.first(),
-  lastName: chance.last(),
-  affiliation: chance.company(),
-}
-const patchPath = '../../routes/collectionsUsers/patch'
-describe('Patch collections users route handler', () => {
-  it('should return success when the request data is correct', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-    const data = JSON.parse(res._getData())
-    expect(res.statusCode).toBe(200)
-    const matchingAuthor = data.authors.find(
-      author => author.userId === submittingAuthor.id,
-    )
-    expect(matchingAuthor.isSubmitting).toBe(body.isSubmitting)
-    expect(matchingAuthor.isCorresponding).toBe(body.isCorresponding)
-    expect(submittingAuthor.firstName).toBe(body.firstName)
-    expect(submittingAuthor.lastName).toBe(body.lastName)
-  })
-  it('should return an error when the params are missing', async () => {
-    delete body.isSubmitting
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Missing parameters')
-    body.isSubmitting = false
-  })
-  it('should return an error if the collection does not exists', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = 'invalid-id'
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the user does not exist', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = author.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = 'invalid-id'
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the collection does not have authors', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = authorsCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Collection does not have any authors')
-  })
-  it('should return an error when the collection and the user do not match', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Collection and user do not match')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/fixtures/collections.js b/packages/component-user-manager/src/tests/fixtures/collections.js
deleted file mode 100644
index bd50ad595d07a565ae5b799fb1a744217b92192d..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/collections.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const Chance = require('chance')
-const { submittingAuthor } = require('./userData')
-
-const chance = new Chance()
-const collections = {
-  standardCollection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [],
-    owners: [submittingAuthor.id],
-    authors: [
-      {
-        userId: submittingAuthor.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
-    save: jest.fn(() => collections.standardCollection),
-  },
-  authorsCollection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [],
-    owners: [submittingAuthor.id],
-    save: jest.fn(),
-  },
-}
-
-module.exports = collections
diff --git a/packages/component-user-manager/src/tests/fixtures/fixtures.js b/packages/component-user-manager/src/tests/fixtures/fixtures.js
deleted file mode 100644
index 0ea29e85ac3a373a20a0e82377e0b6f3dfeef51e..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/fixtures.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const teams = require('./teams')
-
-module.exports = {
-  users,
-  collections,
-  teams,
-}
diff --git a/packages/component-user-manager/src/tests/fixtures/teams.js b/packages/component-user-manager/src/tests/fixtures/teams.js
deleted file mode 100644
index 410b999e3f98f3b196eb28feb5512c744e5b61ea..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/teams.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const { authorTeamID } = require('./teamIDs')
-
-const { standardCollection } = collections
-const { submittingAuthor } = users
-const teams = {
-  authorTeam: {
-    teamType: {
-      name: 'author',
-      permissions: 'author',
-    },
-    group: 'author',
-    name: 'author',
-    object: {
-      type: 'collection',
-      id: standardCollection.id,
-    },
-    members: [submittingAuthor.id],
-    save: jest.fn(() => teams.authorTeam),
-    updateProperties: jest.fn(() => teams.authorTeam),
-    id: authorTeamID,
-  },
-}
-module.exports = teams
diff --git a/packages/component-user-manager/src/tests/fixtures/userData.js b/packages/component-user-manager/src/tests/fixtures/userData.js
deleted file mode 100644
index 1a962c33dc4915763c259edae6deadfab9911f98..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/userData.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-
-module.exports = {
-  author: {
-    id: chance.guid(),
-    email: chance.email(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-  },
-  submittingAuthor: {
-    id: chance.guid(),
-    email: chance.email(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-  },
-  admin: {
-    id: chance.guid(),
-    email: chance.email(),
-  },
-}
diff --git a/packages/component-user-manager/src/tests/fixtures/users.js b/packages/component-user-manager/src/tests/fixtures/users.js
deleted file mode 100644
index 654032921308157e12248205373970f595d0d497..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/users.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const { authorTeamID } = require('./teamIDs')
-const { author, submittingAuthor, admin } = require('./userData')
-const Chance = require('chance')
-
-const chance = new Chance()
-const users = {
-  admin: {
-    type: 'user',
-    username: 'admin',
-    email: admin.email,
-    password: 'password',
-    admin: true,
-    id: admin.id,
-  },
-  author: {
-    type: 'user',
-    username: 'author',
-    email: author.email,
-    password: 'password',
-    admin: false,
-    id: author.id,
-    passwordResetToken: chance.hash(),
-    firstName: author.firstName,
-    lastName: author.lastName,
-    affiliation: 'MIT',
-    title: 'Mr',
-    save: jest.fn(() => users.author),
-    isConfirmed: false,
-    teams: [authorTeamID],
-    updateProperties: jest.fn(() => users.author),
-  },
-  submittingAuthor: {
-    type: 'user',
-    username: 'sauthor',
-    email: submittingAuthor.email,
-    password: 'password',
-    admin: false,
-    id: submittingAuthor.id,
-    passwordResetToken: chance.hash(),
-    firstName: submittingAuthor.firstName,
-    lastName: submittingAuthor.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.submittingAuthor),
-    isConfirmed: false,
-  },
-}
-
-module.exports = users
diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..5bea3f85277e6cadf09fd9ff8b31ae5bbb15df7f
--- /dev/null
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js
@@ -0,0 +1,77 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+const cloneDeep = require('lodash/cloneDeep')
+
+const httpMocks = require('node-mocks-http')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { author, submittingAuthor } = fixtures.users
+const { collection } = fixtures.collections
+const deletePath = '../../routes/fragmentsUsers/delete'
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+describe('Delete fragments users route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when an author is deleted', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = 'invalid-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the user does not exist', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.params.userId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
+})
diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..290d26af0b1efa9ad4a9b59cf519b40c688e9787
--- /dev/null
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js
@@ -0,0 +1,64 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+const cloneDeep = require('lodash/cloneDeep')
+
+const httpMocks = require('node-mocks-http')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { collection } = fixtures.collections
+const { submittingAuthor } = fixtures.users
+
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+const getPath = '../../routes/fragmentsUsers/get'
+describe('Get fragments users route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the request data is correct', async () => {
+    const req = httpMocks.createRequest()
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.user = submittingAuthor.id
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(data).toHaveLength(1)
+    expect(data[0].isSubmitting).toBeDefined()
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.params.collectionId = 'invalid-id'
+    req.user = submittingAuthor.id
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
+})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/post.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
similarity index 61%
rename from packages/component-user-manager/src/tests/collectionsUsers/post.test.js
rename to packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
index eede871a5820c4aba630a48186a8ed88a080d7ab..42ceb286f20f187bde72a6e523527f8d42b85ae9 100644
--- a/packages/component-user-manager/src/tests/collectionsUsers/post.test.js
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
@@ -2,11 +2,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
 
-const models = Model.build()
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
   sendNotificationEmail: jest.fn(),
@@ -14,21 +15,31 @@ jest.mock('pubsweet-component-mail-service', () => ({
 const chance = new Chance()
 
 const { author, submittingAuthor } = fixtures.users
-const { standardCollection } = fixtures.collections
-const postPath = '../../routes/collectionsUsers/post'
-const body = {
+const { collection } = fixtures.collections
+const postPath = '../../routes/fragmentsUsers/post'
+const reqBody = {
   email: chance.email(),
   role: 'author',
   isSubmitting: true,
   isCorresponding: false,
 }
-describe('Post collections users route handler', () => {
-  it('should return success when an author adds a new user to a collection', async () => {
+describe('Post fragments users route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when an author adds a new user to a fragment', async () => {
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -36,36 +47,32 @@ describe('Post collections users route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.email).toEqual(body.email)
     expect(data.invitations).toBeUndefined()
-    const matchingAuthor = standardCollection.authors.find(
-      author => author.userId === data.id,
-    )
-    expect(matchingAuthor).toBeDefined()
   })
-  it('should return success when an author adds an existing user as co author to a collection', async () => {
+  it('should return success when an author adds an existing user as co author to a fragment', async () => {
     body.email = author.email
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
     expect(data.email).toEqual(body.email)
-    const matchingAuthor = standardCollection.authors.find(
-      auth => auth.userId === author.id,
-    )
-    expect(matchingAuthor).toBeDefined()
   })
-  it('should return an error when the an author is added to the same collection', async () => {
+  it('should return an error when the an author is added to the same fragment', async () => {
     body.email = submittingAuthor.email
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -81,7 +88,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -96,7 +105,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -111,7 +122,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -119,4 +132,21 @@ describe('Post collections users route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('invalid-role is not defined')
   })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(postPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
 })
diff --git a/packages/component-user-manager/src/tests/helpers/Model.js b/packages/component-user-manager/src/tests/helpers/Model.js
deleted file mode 100644
index e5f31fa11df30b229f84e129d41c33062f7b6d1d..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/helpers/Model.js
+++ /dev/null
@@ -1,61 +0,0 @@
-const fixtures = require('../fixtures/fixtures')
-
-const UserMock = require('../mocks/User')
-const TeamMock = require('../mocks/Team')
-
-const notFoundError = new Error()
-notFoundError.name = 'NotFoundError'
-notFoundError.status = 404
-
-const build = () => {
-  const models = {
-    User: {},
-    Collection: {
-      find: jest.fn(id => findMock(id, 'collections')),
-    },
-    Team: {},
-  }
-  UserMock.find = jest.fn(id => findMock(id, 'users'))
-  UserMock.findByEmail = jest.fn(email => findByEmailMock(email))
-  UserMock.all = jest.fn(() => Object.values(fixtures.users))
-  UserMock.updateProperties = jest.fn(user =>
-    updatePropertiesMock(user, 'users'),
-  )
-  TeamMock.find = jest.fn(id => findMock(id, 'teams'))
-  TeamMock.updateProperties = jest.fn(team =>
-    updatePropertiesMock(team, 'teams'),
-  )
-  TeamMock.all = jest.fn(() => Object.values(fixtures.teams))
-
-  models.User = UserMock
-  models.Team = TeamMock
-  return models
-}
-
-const findMock = (id, type) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj.id === id,
-  )
-
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-
-const findByEmailMock = email => {
-  const foundUser = Object.values(fixtures.users).find(
-    fixtureUser => fixtureUser.email === email,
-  )
-
-  if (foundUser === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundUser)
-}
-
-const updatePropertiesMock = (obj, type) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj === obj,
-  )
-
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-module.exports = { build }
diff --git a/packages/component-user-manager/src/tests/mocks/Team.js b/packages/component-user-manager/src/tests/mocks/Team.js
deleted file mode 100644
index f84ef4dcf8ffa0e5056c8f0eb86573f624d74c06..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/mocks/Team.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable func-names-any */
-
-function Team(properties) {
-  this.teamType = properties.teamType
-  this.group = properties.group
-  this.name = properties.name
-  this.object = properties.object
-  this.members = properties.members
-}
-
-Team.prototype.save = jest.fn(function saveTeam() {
-  this.id = '111222'
-  return Promise.resolve(this)
-})
-
-module.exports = Team
diff --git a/packages/component-user-manager/src/tests/mocks/User.js b/packages/component-user-manager/src/tests/mocks/User.js
deleted file mode 100644
index 9a7459fc5578d3aa1d5dcec8562fa8f17225b1eb..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/mocks/User.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/* eslint-disable func-names-any */
-
-function User(properties) {
-  this.type = 'user'
-  this.email = properties.email
-  this.username = properties.username
-  this.password = properties.password
-  this.roles = properties.roles
-  this.title = properties.title
-  this.affiliation = properties.affiliation
-  this.firstName = properties.firstName
-  this.lastName = properties.lastName
-  this.admin = properties.admin
-}
-
-User.prototype.save = jest.fn(function saveUser() {
-  this.id = '111222'
-  return Promise.resolve(this)
-})
-
-module.exports = User
diff --git a/packages/component-user-manager/src/tests/users/resetPassword.test.js b/packages/component-user-manager/src/tests/users/resetPassword.test.js
index f2a7522d2874cd428b57896a853943fe8dcb6d9b..12d6e07bc25a0e4a03737d08f09519c1a443d659 100644
--- a/packages/component-user-manager/src/tests/users/resetPassword.test.js
+++ b/packages/component-user-manager/src/tests/users/resetPassword.test.js
@@ -1,25 +1,27 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
-const clone = require('lodash/cloneDeep')
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
 
+const { Model, fixtures } = fixturesService
 const chance = new Chance()
 
-const { author } = fixtures.users
-const clonedAuthor = clone(author)
-
-const body = {
-  email: clonedAuthor.email,
-  firstName: clonedAuthor.firstName,
-  lastName: clonedAuthor.lastName,
-  title: clonedAuthor.title,
-  affiliation: clonedAuthor.affiliation,
+const { user, author } = fixtures.users
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+const reqBody = {
+  email: user.email,
+  firstName: user.firstName,
+  lastName: user.lastName,
+  title: user.title,
+  affiliation: user.affiliation,
   password: 'password',
-  token: clonedAuthor.passwordResetToken,
+  token: user.passwordResetToken,
   isConfirmed: false,
 }
 
@@ -28,69 +30,67 @@ notFoundError.name = 'NotFoundError'
 notFoundError.status = 404
 const resetPasswordPath = '../../routes/users/resetPassword'
 describe('Users password reset route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
   it('should return an error when some parameters are missing', async () => {
     delete body.email
     const req = httpMocks.createRequest({ body })
 
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('missing required params')
-    body.email = author.email
   })
   it('should return an error when the password is too small', async () => {
     body.password = 'small'
     const req = httpMocks.createRequest({ body })
 
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual(
       'password needs to be at least 7 characters long',
     )
-    body.password = 'password'
   })
   it('should return an error when user is not found', async () => {
     body.email = chance.email()
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('user not found')
-    body.email = author.email
   })
   it('should return an error when the tokens do not match', async () => {
     body.token = chance.hash()
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('invalid request')
-    body.token = author.passwordResetToken
   })
   it('should return an error when the user is already confirmed', async () => {
-    author.isConfirmed = true
+    body.email = author.email
+    body.token = author.passwordResetToken
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('User is already confirmed')
-    author.isConfirmed = false
   })
   it('should return success when the body is correct', async () => {
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
 
     expect(res.statusCode).toBe(200)
diff --git a/packages/component-wizard/package.json b/packages/component-wizard/package.json
index 0808310bd41254332a5be4314b576b06da4f8e40..7d2840160e3e7212749ca1b010b0b9ccf687a594 100644
--- a/packages/component-wizard/package.json
+++ b/packages/component-wizard/package.json
@@ -17,7 +17,7 @@
     "react-dom": "^15.6.1",
     "react-router-dom": "^4.2.2",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "recompose": "^0.26.0",
     "xpub-validators": "^0.0.3",
     "xpub-connect": "^0.0.3",
diff --git a/packages/component-wizard/src/components/WizardFormStep.js b/packages/component-wizard/src/components/WizardFormStep.js
index f28662bbe5a2f8f787844e91eefa160121af7087..e87290a13955d609c3f6fd59b93edf7afa502128 100644
--- a/packages/component-wizard/src/components/WizardFormStep.js
+++ b/packages/component-wizard/src/components/WizardFormStep.js
@@ -1,8 +1,8 @@
 import PropTypes from 'prop-types'
 import { connect } from 'react-redux'
-import { debounce, pick, get, isEqual } from 'lodash'
 import { actions } from 'pubsweet-client'
-import { compose, getContext, withProps } from 'recompose'
+import { debounce, pick, get, isEqual } from 'lodash'
+import { compose, getContext, withProps, setDisplayName } from 'recompose'
 import { reduxForm, formValueSelector, SubmissionError } from 'redux-form'
 
 import WizardStep from './WizardStep'
@@ -14,9 +14,8 @@ const onChange = (
   values,
   dispatch,
   { project, version, wizard: { formSectionKeys }, setLoader },
-  prevValues,
 ) => {
-  const prev = pick(prevValues, formSectionKeys)
+  const prev = pick(version, formSectionKeys)
   const newValues = pick(values, formSectionKeys)
   // TODO: fix this if it sucks down the road
   if (!isEqual(prev, newValues)) {
@@ -100,6 +99,7 @@ const onSubmit = (
 }
 
 export default compose(
+  setDisplayName('SubmitWizard'),
   getContext({
     history: PropTypes.object,
     isFinal: PropTypes.bool,
@@ -112,8 +112,8 @@ export default compose(
     toggleConfirmation: PropTypes.func,
   }),
   withProps(({ version, wizard }) => ({
-    initialValues: pick(version, wizard.formSectionKeys),
     readonly: !!get(version, 'submitted'),
+    initialValues: pick(version, wizard.formSectionKeys),
   })),
   connect((state, { wizard: { formSectionKeys } }) => ({
     formValues: wizardSelector(state, ...formSectionKeys),
diff --git a/packages/component-wizard/src/components/WizardStep.js b/packages/component-wizard/src/components/WizardStep.js
index 6846bee3fb582f14a2a89df9f44961796e912ecd..1f8a35281318010fd7cdb2e00a89ce3df6e9c9d3 100644
--- a/packages/component-wizard/src/components/WizardStep.js
+++ b/packages/component-wizard/src/components/WizardStep.js
@@ -39,7 +39,6 @@ export default ({
             validate,
             dependsOn,
             renderComponent: Comp,
-            format,
             parse,
             ...rest
           }) => {
@@ -54,10 +53,9 @@ export default ({
                 <ValidatedField
                   component={input => (
                     <div data-test={fieldId}>
-                      <Comp {...rest} {...input} {...dispatchFns} />{' '}
+                      <Comp {...rest} {...input} {...dispatchFns} />
                     </div>
                   )}
-                  format={format}
                   name={fieldId}
                   parse={parse}
                   validate={validate}
diff --git a/packages/component-wizard/src/redux/conversion.js b/packages/component-wizard/src/redux/conversion.js
index c085f1e196ddd80a3cf4449212c088353b6807ad..87f5ac3843b481f59c5b0ca9584266b3049ed3a0 100644
--- a/packages/component-wizard/src/redux/conversion.js
+++ b/packages/component-wizard/src/redux/conversion.js
@@ -1,5 +1,5 @@
-import { pick } from 'lodash'
 import moment from 'moment'
+import { pick } from 'lodash'
 import { actions } from 'pubsweet-client'
 import { create } from 'pubsweet-client/src/helpers/api'
 
@@ -24,13 +24,13 @@ const generateCustomId = () =>
     .toString()
     .slice(-7)
 
-const addSubmittingAuthor = (user, collectionId) => {
+const addSubmittingAuthor = (user, collectionId, fragmentId) => {
   const author = {
-    ...pick(user, ['affiliation', 'email', 'firstName', 'lastName']),
+    ...pick(user, ['id', 'email', 'affiliation', 'firstName', 'lastName']),
     isSubmitting: true,
     isCorresponding: true,
   }
-  create(`/collections/${collectionId}/users`, {
+  create(`/collections/${collectionId}/fragments/${fragmentId}/users`, {
     role: 'author',
     ...author,
   })
@@ -50,6 +50,7 @@ export const createDraftSubmission = history => (dispatch, getState) => {
     return dispatch(
       actions.createFragment(collection, {
         created: new Date(), // TODO: set on server
+        collectionId: collection.id,
         files: {
           manuscripts: [],
           supplementary: [],
@@ -65,7 +66,7 @@ export const createDraftSubmission = history => (dispatch, getState) => {
       }
       const route = `/projects/${collection.id}/versions/${fragment.id}/submit`
       if (!currentUser.admin) {
-        addSubmittingAuthor(currentUser, collection.id)
+        addSubmittingAuthor(currentUser, collection.id, fragment.id)
       }
 
       // redirect after a short delay
@@ -76,6 +77,27 @@ export const createDraftSubmission = history => (dispatch, getState) => {
   })
 }
 
+export const createRevision = (
+  collection,
+  previousVersion,
+  history,
+) => dispatch => {
+  const { id, submitted, ...prev } = previousVersion
+  return dispatch(
+    actions.createFragment(collection, {
+      ...prev,
+      created: new Date(),
+      version: previousVersion.version + 1,
+    }),
+  ).then(({ fragment }) => {
+    const route = `/projects/${collection.id}/versions/${fragment.id}/submit`
+    window.setTimeout(() => {
+      history.push(route)
+    }, 10)
+    return fragment
+  })
+}
+
 /* reducer */
 const initialState = {
   complete: undefined,
diff --git a/packages/components-faraday/package.json b/packages/components-faraday/package.json
index 030679184f97ef574310dda6920ecfeaaf55f8bc..30188c9434fbf453d0fe3c775c6e7af55cf8cc26 100644
--- a/packages/components-faraday/package.json
+++ b/packages/components-faraday/package.json
@@ -16,7 +16,7 @@
     "react-tippy": "^1.2.2",
     "recompose": "^0.26.0",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "styled-components": "^3.1.6"
   }
 }
diff --git a/packages/components-faraday/src/components/AuthorList/Author.js b/packages/components-faraday/src/components/AuthorList/Author.js
index 5df7654e9b1ef7277a196a6981dbff69e8c2669b..d993d10b1074579a766a60f75e880cd07a00c3cd 100644
--- a/packages/components-faraday/src/components/AuthorList/Author.js
+++ b/packages/components-faraday/src/components/AuthorList/Author.js
@@ -19,6 +19,7 @@ export default ({
   setAuthorEdit,
   isCorresponding,
   parseAuthorType,
+  ...rest
 }) => (
   <Root isOver={isOver}>
     {!isOver && dragHandle}
@@ -27,10 +28,7 @@ export default ({
         <Title>{parseAuthorType(isSubmitting, isCorresponding, index)}</Title>
         <ButtonContainer>
           {!isSubmitting && (
-            <ClickableIcon
-              onClick={removeAuthor(id, email)}
-              title="Delete author"
-            >
+            <ClickableIcon onClick={removeAuthor(id)} title="Delete author">
               <Icon size={3}>trash</Icon>
             </ClickableIcon>
           )}
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
index 47264445eab33b0bc90a9a17bc7e84fa77a05aef..82f7305564a4aae1b3cd23b14e01421bfa7f7e4e 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
@@ -1,16 +1,15 @@
 import React from 'react'
 import { get } from 'lodash'
 import { connect } from 'react-redux'
-import { reduxForm } from 'redux-form'
 import styled from 'styled-components'
 import { Button, th } from '@pubsweet/ui'
-import { compose, withProps } from 'recompose'
 import { selectCurrentUser } from 'xpub-selectors'
+import { reduxForm, change as changeForm } from 'redux-form'
+import { compose, withProps, setDisplayName } from 'recompose'
 
 import { emailValidator } from '../utils'
 import { Spinner } from '../UIComponents/'
 import {
-  getAuthors,
   authorSuccess,
   authorFailure,
   getAuthorFetching,
@@ -77,6 +76,7 @@ export default compose(
       currentUser: selectCurrentUser(state),
     }),
     {
+      changeForm,
       authorSuccess,
       authorFailure,
     },
@@ -101,38 +101,36 @@ export default compose(
   reduxForm({
     form: 'author',
     enableReinitialize: true,
+    destroyOnUnmount: false,
     onSubmit: (
       values,
       dispatch,
-      { authors = [], addAuthor, setEditMode, setFormAuthors, reset, match },
+      {
+        reset,
+        match,
+        changeForm,
+        addAuthor,
+        setEditMode,
+        setFormAuthors,
+        authors = [],
+      },
     ) => {
       const collectionId = get(match, 'params.project')
+      const fragmentId = get(match, 'params.version')
       const isFirstAuthor = authors.length === 0
-      addAuthor(
-        {
-          ...values,
-          isSubmitting: isFirstAuthor,
-          isCorresponding: isFirstAuthor,
-        },
-        collectionId,
-      ).then(
-        () => {
-          setEditMode(false)()
-          setTimeout(() => {
-            getAuthors(collectionId).then(
-              data => {
-                dispatch(authorSuccess())
-                setFormAuthors(data)
-              },
-              err => dispatch(authorFailure(err)),
-            )
-          }, 10)
-          reset()
-        },
-        err => dispatch(authorFailure(err)),
-      )
+      const newAuthor = {
+        ...values,
+        isSubmitting: isFirstAuthor,
+        isCorresponding: isFirstAuthor,
+      }
+      addAuthor(newAuthor, collectionId, fragmentId).then(author => {
+        changeForm('wizard', 'authors', [...authors, author])
+        setEditMode(false)()
+        reset()
+      })
     },
   }),
+  setDisplayName('AuthorAdder'),
 )(AuthorAdder)
 
 // #region styled-components
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
index 38deb2e6199096c7714b31fcdbbf9ecd0db8fe15..185296d68773dbb6c6abd6cbdad0d1d5b7acbb95 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
@@ -3,25 +3,26 @@ import { pick } from 'lodash'
 import { connect } from 'react-redux'
 import styled, { css } from 'styled-components'
 import { Icon, Checkbox, th } from '@pubsweet/ui'
-import { compose, withHandlers, withProps } from 'recompose'
+import { compose, withProps } from 'recompose'
 import { reduxForm, Field, change as changeForm } from 'redux-form'
 
 import { Spinner } from '../UIComponents'
 
 import {
-  getAuthors,
-  editAuthor,
   authorSuccess,
   authorFailure,
   getAuthorFetching,
 } from '../../redux/authors'
 import { ValidatedTextField, Label } from './FormItems'
 
+import { authorKeys, parseEditedAuthors } from './utils'
+
 const renderCheckbox = ({ input }) => (
   <Checkbox checked={input.value} type="checkbox" {...input} />
 )
 
 const AuthorEdit = ({
+  id,
   index,
   email,
   isFetching,
@@ -30,8 +31,6 @@ const AuthorEdit = ({
   setAuthorEdit,
   parseAuthorType,
   isCorresponding,
-  changeCorresponding,
-  ...rest
 }) => (
   <Root>
     <Header>
@@ -39,11 +38,7 @@ const AuthorEdit = ({
         <span>{parseAuthorType(isSubmitting, isCorresponding, index)}</span>
         {!isSubmitting && (
           <Fragment>
-            <Field
-              component={renderCheckbox}
-              name="edit.isCorresponding"
-              onChange={changeCorresponding(email)}
-            />
+            <Field component={renderCheckbox} name="edit.isCorresponding" />
             <label>Corresponding</label>
           </Fragment>
         )}
@@ -92,47 +87,19 @@ export default compose(
   ),
   withProps(props => ({
     initialValues: {
-      edit: pick(props, [
-        'id',
-        'email',
-        'lastName',
-        'firstName',
-        'affiliation',
-        'isSubmitting',
-        'isCorresponding',
-      ]),
+      edit: pick(props, authorKeys),
     },
   })),
-  withHandlers({
-    changeCorresponding: ({ changeForm, setAsCorresponding }) => email => (
-      evt,
-      newValue,
-    ) => {
-      setAsCorresponding(email)()
-      changeForm('edit', 'edit.isCorresponding', newValue)
-    },
-  }),
   reduxForm({
     form: 'edit',
     onSubmit: (
-      values,
+      { edit: newAuthor },
       dispatch,
-      { setAuthorEdit, setAuthors, authors, index, changeForm, project },
+      { authors, changeForm, setAuthorEdit },
     ) => {
-      const newAuthor = values.edit
-      editAuthor(project.id, newAuthor.id, newAuthor).then(
-        () => {
-          getAuthors(project.id).then(
-            data => {
-              dispatch(authorSuccess())
-              setAuthorEdit(-1)()
-              setAuthors(data)
-            },
-            err => dispatch(authorFailure(err)),
-          )
-        },
-        err => dispatch(authorFailure(err)),
-      )
+      const newAuthors = parseEditedAuthors(newAuthor, authors)
+      changeForm('wizard', 'authors', newAuthors)
+      setAuthorEdit(-1)()
     },
   }),
 )(AuthorEdit)
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorList.js b/packages/components-faraday/src/components/AuthorList/AuthorList.js
index c6b03ef04c98938244acd3f7731886b24b9444ab..5fd5b2708393789403b3061c08e08d13150e3125 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorList.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorList.js
@@ -1,4 +1,5 @@
 import React from 'react'
+import { get, isBoolean, isNumber } from 'lodash'
 import { th } from '@pubsweet/ui'
 import PropTypes from 'prop-types'
 import { connect } from 'react-redux'
@@ -6,35 +7,33 @@ import styled from 'styled-components'
 import { withRouter } from 'react-router-dom'
 import {
   compose,
-  lifecycle,
   withState,
+  withProps,
   getContext,
   withHandlers,
+  setDisplayName,
 } from 'recompose'
-import { change as changeForm } from 'redux-form'
+import { change as changeForm, formValueSelector } from 'redux-form'
 import { SortableList } from 'pubsweet-component-sortable-list/src/components'
 
 import {
   addAuthor,
-  getAuthors,
   deleteAuthor,
   authorFailure,
-  getAuthorsTeam,
   getAuthorError,
-  updateAuthorsTeam,
 } from '../../redux/authors'
 
-import Author from './Author'
-import StaticList from './StaticList'
-import AuthorAdder from './AuthorAdder'
 import { DragHandle } from './FormItems'
-import AuthorEditor from './AuthorEditor'
+import { Author, StaticList, AuthorAdder, AuthorEditor } from './'
+
+const wizardSelector = formValueSelector('wizard')
 
 const Authors = ({
   match,
   error,
   authors,
   version,
+  addMode,
   editMode,
   dropItem,
   addAuthor,
@@ -50,25 +49,25 @@ const Authors = ({
       addAuthor={addAuthor}
       authors={authors}
       editAuthor={editAuthor}
-      editMode={editMode}
+      editMode={addMode}
       match={match}
       setEditMode={setEditMode}
       setFormAuthors={setFormAuthors}
     />
-    {editedAuthor > -1 ? (
+    {isNumber(editMode) && editMode > -1 ? (
       <StaticList
         authors={authors}
         editComponent={AuthorEditor}
-        editIndex={editedAuthor}
+        editIndex={editMode}
         setFormAuthors={setFormAuthors}
+        version={version}
         {...rest}
       />
     ) : (
       <SortableList
         beginDragProps={['index', 'lastName']}
         dragHandle={DragHandle}
-        dropItem={dropItem}
-        editedAuthor={editedAuthor}
+        editedAuthor={editMode}
         items={authors || []}
         listItem={Author}
         moveItem={moveAuthor}
@@ -84,8 +83,9 @@ export default compose(
   getContext({ version: PropTypes.object, project: PropTypes.object }),
   connect(
     state => ({
-      currentUser: state.currentUser.user,
       error: getAuthorError(state),
+      currentUser: get(state, 'currentUser.user'),
+      authorForm: wizardSelector(state, 'authorForm'),
     }),
     {
       addAuthor,
@@ -95,86 +95,49 @@ export default compose(
     },
   ),
   withState('authors', 'setAuthors', []),
-  withState('editMode', 'setEditMode', false),
-  withState('editedAuthor', 'setEditedAuthor', -1),
+  withProps(({ version, authorForm }) => ({
+    authors: get(version, 'authors') || [],
+    addMode: isBoolean(authorForm) && authorForm,
+    editMode: isNumber(authorForm) ? authorForm : -1,
+  })),
   withHandlers({
-    setFormAuthors: ({ setAuthors, changeForm }) => authors => {
+    setFormAuthors: ({ setAuthors, changeForm }) => (authors = []) => {
       setAuthors(authors)
       changeForm('wizard', 'authors', authors)
     },
   }),
   withHandlers({
-    setAuthorEdit: ({ setEditedAuthor, changeForm }) => editedAuthor => e => {
+    setAuthorEdit: ({ changeForm }) => authorIndex => e => {
       e && e.preventDefault && e.preventDefault()
-      changeForm('wizard', 'editMode', editedAuthor > -1)
-      setEditedAuthor(prev => editedAuthor)
+      changeForm('wizard', 'authorForm', authorIndex)
     },
-    setEditMode: ({ setEditMode, changeForm }) => mode => e => {
+    setEditMode: ({ changeForm }) => mode => e => {
       e && e.preventDefault()
-      changeForm('wizard', 'editMode', mode)
-      setEditMode(v => mode)
-    },
-    dropItem: ({ authors, setFormAuthors, project, authorFailure }) => () => {
-      setFormAuthors(authors)
-      getAuthorsTeam(project.id)
-        .then(team => {
-          const members = authors.map(a => a.id)
-          updateAuthorsTeam(team.id, { members }).catch(err => {
-            authorFailure(err)
-            getAuthors(project.id).then(setFormAuthors)
-          })
-        })
-        .catch(err => {
-          authorFailure(err)
-          getAuthors(project.id).then(setFormAuthors)
-        })
+      changeForm('wizard', 'authorForm', mode)
     },
     parseAuthorType: () => (isSubmitting, isCorresponding, index) => {
       if (isSubmitting) return `#${index + 1} Submitting author`
       if (isCorresponding) return `#${index + 1} Corresponding author`
       return `#${index + 1} Author`
     },
-    moveAuthor: ({ authors, setFormAuthors, changeForm }) => (
-      dragIndex,
-      hoverIndex,
-    ) => {
+    moveAuthor: ({ authors, setFormAuthors }) => (dragIndex, hoverIndex) => {
       const newAuthors = SortableList.moveItem(authors, dragIndex, hoverIndex)
       setFormAuthors(newAuthors)
     },
     removeAuthor: ({
       authors,
+      version,
       project,
       deleteAuthor,
-      authorFailure,
       setFormAuthors,
-    }) => (id, authorEmail) => () => {
-      deleteAuthor(project.id, id).then(
-        () => {
-          const newAuthors = authors.filter(a => a.id !== id)
-          setFormAuthors(newAuthors)
-        },
-        err => authorFailure(err),
-      )
-    },
-    setAsCorresponding: ({ authors, setFormAuthors }) => authorEmail => () => {
-      const newAuthors = authors.map(
-        a =>
-          a.email === authorEmail
-            ? {
-                ...a,
-                isCorresponding: !a.isCorresponding,
-              }
-            : { ...a, isCorresponding: false },
-      )
-      setFormAuthors(newAuthors)
-    },
-  }),
-  lifecycle({
-    componentDidMount() {
-      const { setFormAuthors, project } = this.props
-      getAuthors(project.id).then(setFormAuthors)
+    }) => id => () => {
+      deleteAuthor(project.id, version.id, id).then(() => {
+        const newAuthors = authors.filter(a => a.id !== id)
+        setFormAuthors(newAuthors)
+      })
     },
   }),
+  setDisplayName('AuthorList'),
 )(Authors)
 
 // #region styled-components
diff --git a/packages/components-faraday/src/components/AuthorList/StaticList.js b/packages/components-faraday/src/components/AuthorList/StaticList.js
index 6e00a7a456175175eb4953ddfa8a82671c82d540..f6f82fc5bd649641563777bceb170b1e161847e5 100644
--- a/packages/components-faraday/src/components/AuthorList/StaticList.js
+++ b/packages/components-faraday/src/components/AuthorList/StaticList.js
@@ -3,14 +3,15 @@ import React from 'react'
 import Author from './Author'
 
 export default ({
+  version,
+  project,
   authors,
   editIndex,
-  setFormAuthors,
   removeAuthor,
   editComponent,
   setAuthorEdit,
+  setFormAuthors,
   parseAuthorType,
-  setAsCorresponding,
   ...rest
 }) => (
   <div>
@@ -19,17 +20,17 @@ export default ({
         index === editIndex ? (
           React.createElement(editComponent, {
             key: 'author-editor',
-            authors,
             index,
             initialValues: {
               edit: author,
             },
+            authors,
             setAuthors: setFormAuthors,
             setAuthorEdit,
             parseAuthorType,
-            setAsCorresponding,
+            project,
+            version,
             ...author,
-            ...rest,
           })
         ) : (
           <Author
@@ -38,7 +39,6 @@ export default ({
             index={index}
             parseAuthorType={parseAuthorType}
             removeAuthor={removeAuthor}
-            setAsCorresponding={setAsCorresponding}
             {...rest}
           />
         ),
diff --git a/packages/components-faraday/src/components/AuthorList/index.js b/packages/components-faraday/src/components/AuthorList/index.js
index 473f04dcd0b9fba6f38dd46e866126960656c2f2..1ff2016c20ac9622dde7315255596cff24fea844 100644
--- a/packages/components-faraday/src/components/AuthorList/index.js
+++ b/packages/components-faraday/src/components/AuthorList/index.js
@@ -1 +1,9 @@
+import * as utils from './utils'
+
+export { default as Author } from './Author'
 export { default as AuthorList } from './AuthorList'
+export { default as StaticList } from './StaticList'
+export { default as AuthorAdder } from './AuthorAdder'
+export { default as AuthorEditor } from './AuthorEditor'
+
+export { utils }
diff --git a/packages/components-faraday/src/components/AuthorList/utils.js b/packages/components-faraday/src/components/AuthorList/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..e58c4abec7befcf31d2d74dc9c4ecf0ceb324ab0
--- /dev/null
+++ b/packages/components-faraday/src/components/AuthorList/utils.js
@@ -0,0 +1,40 @@
+import { isBoolean } from 'lodash'
+
+export const authorKeys = [
+  'id',
+  'email',
+  'lastName',
+  'firstName',
+  'affiliation',
+  'isSubmitting',
+  'isCorresponding',
+]
+
+export const setCorresponding = id => author =>
+  author.id === id
+    ? {
+        ...author,
+        isCorresponding: true,
+      }
+    : { ...author, isCorresponding: false }
+
+export const castToBool = author => ({
+  ...author,
+  isCorresponding: isBoolean(author.isCorresponding) && author.isCorresponding,
+})
+
+export const parseEditedAuthors = (editedAuthor, authors) => {
+  const newAuthor = castToBool(editedAuthor)
+
+  return authors.map(
+    a =>
+      a.id === newAuthor.id
+        ? newAuthor
+        : {
+            ...a,
+            isCorresponding: newAuthor.isCorresponding
+              ? false
+              : a.isCorresponding,
+          },
+  )
+}
diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js
index f773064fb9af6d65f85921d6e28d653aa53a9e2a..ae4c120f523173860f19fb00bb4352a05c9fb97e 100644
--- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js
+++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js
@@ -16,8 +16,8 @@ import { selectInvitation } from '../../redux/reviewers'
 import { ReviewerDecision, HandlingEditorSection, DeleteManuscript } from './'
 import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils'
 import {
-  currentUserIs,
   canMakeDecision,
+  isHEToManuscript,
   canInviteReviewers,
   canMakeRecommendation,
 } from '../../../../component-faraday-selectors/src'
@@ -85,18 +85,19 @@ const DashboardCard = ({
                   <Icon>download</Icon>
                 </ClickableIcon>
               </ZipFiles>
-              {!project.status && (
-                <ActionButtons
-                  data-test="button-resume-submission"
-                  onClick={() =>
-                    history.push(
-                      `/projects/${project.id}/versions/${version.id}/submit`,
-                    )
-                  }
-                >
-                  RESUME SUBMISSION
-                </ActionButtons>
-              )}
+              {!project.status ||
+                (project.status === 'draft' && (
+                  <ActionButtons
+                    data-test="button-resume-submission"
+                    onClick={() =>
+                      history.push(
+                        `/projects/${project.id}/versions/${version.id}/submit`,
+                      )
+                    }
+                  >
+                    RESUME SUBMISSION
+                  </ActionButtons>
+                ))}
             </RightDetails>
           </LeftDetails>
         </Top>
@@ -117,7 +118,7 @@ const DashboardCard = ({
             <ManuscriptType title={manuscriptMeta}>
               {manuscriptMeta}
             </ManuscriptType>
-            {project.status ? (
+            {project.status && project.status !== 'draft' ? (
               <Details
                 data-test="button-details"
                 onClick={() =>
@@ -138,45 +139,48 @@ const DashboardCard = ({
           </RightDetails>
         </Bottom>
       </ListView>
-      {project.status && (
-        <DetailsView>
-          <Top>
-            <AuthorsWithTooltip authors={project.authors} />
-          </Top>
-          <Bottom>
-            <LeftDetails flex={4}>
-              <HandlingEditorSection
-                currentUser={currentUser}
-                project={project}
-              />
-            </LeftDetails>
-            {canInviteReviewers && (
-              <RightDetails flex={4}>
-                <ReviewerBreakdown
-                  collectionId={project.id}
-                  compact
-                  versionId={version.id}
-                />
-                <InviteReviewers
-                  modalKey={`invite-reviewers-${project.id}`}
-                  project={project}
-                  version={version}
-                />
-              </RightDetails>
-            )}
-            {invitation && (
-              <RightDetails flex={4}>
-                <ReviewerText>Invited to review</ReviewerText>
-                <ReviewerDecision
-                  invitation={invitation}
-                  modalKey={`reviewer-decision-${project.id}`}
+      {project.status &&
+        project.status !== 'draft' && (
+          <DetailsView>
+            <Top>
+              <AuthorsWithTooltip authors={project.authors} />
+            </Top>
+            <Bottom>
+              <LeftDetails flex={4}>
+                <HandlingEditorSection
+                  currentUser={currentUser}
+                  isHE={isHE}
                   project={project}
                 />
-              </RightDetails>
-            )}
-          </Bottom>
-        </DetailsView>
-      )}
+              </LeftDetails>
+              {canInviteReviewers && (
+                <RightDetails flex={4}>
+                  <ReviewerBreakdown
+                    collectionId={project.id}
+                    compact
+                    versionId={version.id}
+                  />
+                  <InviteReviewers
+                    modalKey={`invite-reviewers-${project.id}`}
+                    project={project}
+                    version={version}
+                  />
+                </RightDetails>
+              )}
+              {invitation && (
+                <RightDetails flex={4}>
+                  <ReviewerText>Invited to review</ReviewerText>
+                  <ReviewerDecision
+                    invitation={invitation}
+                    modalKey={`reviewer-decision-${project.id}`}
+                    project={project}
+                    version={version}
+                  />
+                </RightDetails>
+              )}
+            </Bottom>
+          </DetailsView>
+        )}
     </Card>
   ) : null
 }
@@ -185,11 +189,11 @@ export default compose(
   setDisplayName('DashboardCard'),
   getContext({ journal: PropTypes.object, currentUser: PropTypes.object }),
   withTheme,
-  connect((state, { project }) => ({
-    isHE: currentUserIs(state, 'handlingEditor'),
-    invitation: selectInvitation(state, project.id),
+  connect((state, { project, version }) => ({
     canMakeDecision: canMakeDecision(state, project),
+    isHE: isHEToManuscript(state, get(project, 'id')),
     canInviteReviewers: canInviteReviewers(state, project),
+    invitation: selectInvitation(state, get(version, 'id')),
     canMakeRecommendation: canMakeRecommendation(state, project),
   })),
 )(DashboardCard)
diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
index b48d96eb07e3fecd249ab622ef560a4e4bce97a5..a77e2e32f7d1d83971d4b8268dc5323aefd27161 100644
--- a/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
+++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
@@ -4,11 +4,11 @@ import { th } from '@pubsweet/ui'
 import styled, { css } from 'styled-components'
 import { EditorInChiefActions, HandlingEditorActions } from './'
 
-const renderHE = (currentUser, project) => {
+const renderHE = (currentUser, isHE, project) => {
   const status = get(project, 'status') || 'draft'
   const isAdmin = get(currentUser, 'admin')
   const isEic = get(currentUser, 'editorInChief')
-  const isHe = get(currentUser, 'handlingEditor')
+
   const handlingEditor = get(project, 'handlingEditor')
   const eicActionsStatuses = ['submitted', 'heInvited']
   const heActionsStatuses = ['heInvited']
@@ -22,7 +22,7 @@ const renderHE = (currentUser, project) => {
     )
   }
 
-  if (isHe && heActionsStatuses.includes(status)) {
+  if (isHE && heActionsStatuses.includes(status)) {
     return (
       <HandlingEditorActions
         currentUser={currentUser}
@@ -35,10 +35,10 @@ const renderHE = (currentUser, project) => {
   return <AssignedHE>{get(handlingEditor, 'name') || 'N/A'}</AssignedHE>
 }
 
-const HandlingEditorSection = ({ currentUser, project }) => (
+const HandlingEditorSection = ({ isHE, currentUser, project }) => (
   <Root>
     <HEText>Handling Editor</HEText>
-    {renderHE(currentUser, project)}
+    {renderHE(currentUser, isHE, project)}
   </Root>
 )
 
diff --git a/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js b/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
index 7037dbebfdfebb3884315b8f9fb030920100eb61..08bfbff9dc1dbd96821f8b40eb6e881604117aea 100644
--- a/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
+++ b/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
@@ -30,20 +30,23 @@ const ModalComponent = connect(state => ({
 export default compose(
   connect(null, {
     reviewerDecision,
+    getFragments: actions.getFragments,
     getCollections: actions.getCollections,
   }),
   withModal(props => ({
     modalComponent: ModalComponent,
   })),
   withHandlers({
-    decisionSuccess: ({ getCollections, hideModal }) => () => {
+    decisionSuccess: ({ getFragments, getCollections, hideModal }) => () => {
       getCollections()
+      getFragments()
       hideModal()
     },
   }),
   withHandlers({
     showAcceptModal: ({
       project,
+      version,
       showModal,
       invitation,
       setModalError,
@@ -54,7 +57,7 @@ export default compose(
         title: 'Agree to review Manuscript?',
         confirmText: 'Agree',
         onConfirm: () => {
-          reviewerDecision(invitation.id, project.id, true).then(
+          reviewerDecision(invitation.id, project.id, version.id, true).then(
             decisionSuccess,
             handleError(setModalError),
           )
@@ -63,6 +66,7 @@ export default compose(
     },
     showDeclineModal: ({
       project,
+      version,
       showModal,
       invitation,
       setModalError,
@@ -73,7 +77,7 @@ export default compose(
         title: 'Decline to review Manuscript?',
         confirmText: 'Decline',
         onConfirm: () => {
-          reviewerDecision(invitation.id, project.id, false).then(
+          reviewerDecision(invitation.id, project.id, version.id, false).then(
             decisionSuccess,
             handleError(setModalError),
           )
diff --git a/packages/components-faraday/src/components/Files/Files.js b/packages/components-faraday/src/components/Files/Files.js
index 4a8d2ff824f0276f29527a2e9e94d8c94a2fcbe1..9ce14c3240d6c3b78edd4f6f13e9ebbdbc8527cd 100644
--- a/packages/components-faraday/src/components/Files/Files.js
+++ b/packages/components-faraday/src/components/Files/Files.js
@@ -65,13 +65,25 @@ const Files = ({
       changeList={changeList}
       dropSortableFile={dropSortableFile}
       files={get(files, 'coverLetter') || []}
-      isLast
       listId="coverLetter"
       maxFiles={1}
       moveItem={moveItem('coverLetter')}
       removeFile={removeFile('coverLetter')}
       title="Cover letter"
     />
+    <FileSection
+      addFile={addFile('coverLetter')}
+      allowedFileExtensions={['pdf', 'doc', 'docx']}
+      changeList={changeList}
+      dropSortableFile={dropSortableFile}
+      files={get(files, 'response') || []}
+      isLast
+      listId="responseToReviewer"
+      maxFiles={1}
+      moveItem={moveItem('response')}
+      removeFile={removeFile('response')}
+      title="Response to reviewers"
+    />
   </div>
 )
 
diff --git a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
index 6ced230390ad0b30dc5de6af1d9ff6baf53ef5be..d67820dc3764e31d5e71fd0ce0275185d662427d 100644
--- a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
+++ b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
@@ -31,7 +31,7 @@ export default compose(
     collection: selectCollection(state, collectionId),
   })),
   withHandlers({
-    getCompactReport: ({ collection: { invitations = [] } }) => () => {
+    getCompactReport: ({ fragment: { invitations = [] } }) => () => {
       const reviewerInvitations = invitations.filter(roleFilter('reviewer'))
       const accepted = reviewerInvitations.filter(acceptedInvitationFilter)
         .length
diff --git a/packages/components-faraday/src/components/MakeDecision/DecisionForm.js b/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
index fe8f3f38b9d92fd7d7b43bc282aa31509a469a7c..0a53c9423317ac06571ebf6470b6ee16b957fb0f 100644
--- a/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
+++ b/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
@@ -1,24 +1,19 @@
 import React from 'react'
 import { get } from 'lodash'
 import { connect } from 'react-redux'
+import styled from 'styled-components'
 import { actions } from 'pubsweet-client'
 import { required } from 'xpub-validators'
-import styled from 'styled-components'
 import { reduxForm, formValueSelector } from 'redux-form'
 import { compose, setDisplayName, withProps } from 'recompose'
-import { Icon, Button, Spinner, RadioGroup, ValidatedField } from '@pubsweet/ui'
+import { Icon, Button, RadioGroup, ValidatedField } from '@pubsweet/ui'
 
 import { FormItems } from '../UIComponents'
-import {
-  selectError,
-  selectFetching,
-  createRecommendation,
-} from '../../redux/recommendations'
+import { createRecommendation } from '../../redux/recommendations'
 import { subtitleParser, decisions, parseFormValues } from './utils'
 import { getHERecommendation } from '../../../../component-faraday-selectors'
 
 const {
-  Err,
   Row,
   Title,
   Label,
@@ -35,9 +30,7 @@ const DecisionForm = ({
   aHERec,
   decision,
   hideModal,
-  isFetching,
   handleSubmit,
-  recommendationError,
   heRecommendation: { reason, message = '' },
 }) => (
   <Form onSubmit={handleSubmit}>
@@ -90,25 +83,14 @@ const DecisionForm = ({
         </RowItem>
       </Row>
     )}
-    {recommendationError && (
-      <Row>
-        <RowItem centered>
-          <Err>{recommendationError}</Err>
-        </RowItem>
-      </Row>
-    )}
     <Row>
       <RowItem centered>
         <Button onClick={hideModal}>Cancel</Button>
       </RowItem>
       <RowItem centered>
-        {isFetching ? (
-          <Spinner size={3} />
-        ) : (
-          <Button primary type="submit">
-            Submit
-          </Button>
-        )}
+        <Button primary type="submit">
+          Submit
+        </Button>
       </RowItem>
     </Row>
   </Form>
@@ -119,12 +101,14 @@ export default compose(
   setDisplayName('DecisionForm'),
   connect(
     (state, { fragmentId, collectionId }) => ({
-      isFetching: selectFetching(state),
       decision: selector(state, 'decision'),
-      recommendationError: selectError(state),
       heRecommendation: getHERecommendation(state, collectionId, fragmentId),
     }),
-    { createRecommendation, getCollections: actions.getCollections },
+    {
+      createRecommendation,
+      getFragments: actions.getFragments,
+      getCollections: actions.getCollections,
+    },
   ),
   withProps(({ heRecommendation: { recommendation = '', comments = [] } }) => ({
     heRecommendation: {
@@ -142,21 +126,25 @@ export default compose(
         hideModal,
         fragmentId,
         collectionId,
+        getFragments,
         getCollections,
         createRecommendation,
       },
     ) => {
       const recommendation = parseFormValues(values)
-      createRecommendation(collectionId, fragmentId, recommendation).then(r => {
-        showModal({
-          onCancel: () => {
-            getCollections()
-            hideModal()
-          },
-          title: 'Decision submitted',
-          cancelText: 'OK',
-        })
-      })
+      createRecommendation(collectionId, fragmentId, recommendation).then(
+        () => {
+          showModal({
+            onCancel: () => {
+              getCollections()
+              getFragments()
+              hideModal()
+            },
+            title: 'Decision submitted',
+            cancelText: 'OK',
+          })
+        },
+      )
     },
   }),
 )(DecisionForm)
diff --git a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
index 3ef84572bbad69a7e64ecb89dd16d60586f5f8dc..fb7452baf0ba87d8e6d3edb530511df05a9a4bfa 100644
--- a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
+++ b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
@@ -9,11 +9,7 @@ import { getFormValues, reset as resetForm } from 'redux-form'
 
 import { FormItems } from '../UIComponents'
 import { StepOne, StepTwo, utils } from './'
-import {
-  selectError,
-  selectFetching,
-  createRecommendation,
-} from '../../redux/recommendations'
+import { createRecommendation } from '../../redux/recommendations'
 
 const RecommendWizard = ({
   step,
@@ -22,8 +18,6 @@ const RecommendWizard = ({
   prevStep,
   closeModal,
   submitForm,
-  isFetching,
-  recommendationError,
   ...rest
 }) => (
   <FormItems.RootContainer>
@@ -39,13 +33,7 @@ const RecommendWizard = ({
       />
     )}
     {step === 1 && (
-      <StepTwo
-        decision={decision}
-        goBack={prevStep}
-        isFetching={isFetching}
-        onSubmit={submitForm}
-        recommendationError={recommendationError}
-      />
+      <StepTwo decision={decision} goBack={prevStep} onSubmit={submitForm} />
     )}
   </FormItems.RootContainer>
 )
@@ -53,13 +41,12 @@ const RecommendWizard = ({
 export default compose(
   connect(
     state => ({
-      isFetching: selectFetching(state),
-      recommendationError: selectError(state),
       decision: get(getFormValues('recommendation')(state), 'decision'),
     }),
     {
       resetForm,
       createRecommendation,
+      getFragments: actions.getFragments,
       getCollections: actions.getCollections,
     },
   ),
@@ -77,6 +64,7 @@ export default compose(
       resetForm,
       fragmentId,
       collectionId,
+      getFragments,
       getCollections,
       createRecommendation,
     }) => values => {
@@ -88,6 +76,7 @@ export default compose(
           cancelText: 'OK',
           onCancel: () => {
             getCollections()
+            getFragments()
             hideModal()
           },
         })
diff --git a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
index b17b7d65b6bcd38bd26baeeff50e58ad583795e6..b6087f87534c4a756da957e77144c91b7d026286 100644
--- a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
+++ b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
@@ -36,11 +36,12 @@ const InviteReviewersModal = compose(
   ),
   withHandlers({
     getReviewers: ({
+      versionId,
       collectionId,
       setReviewers,
       getCollectionReviewers,
     }) => () => {
-      getCollectionReviewers(collectionId)
+      getCollectionReviewers(collectionId, versionId)
     },
     closeModal: ({ getCollections, hideModal }) => () => {
       getCollections()
@@ -81,6 +82,7 @@ const InviteReviewersModal = compose(
         isFetching={fetchingInvite}
         reviewerError={reviewerError}
         reviewers={reviewers}
+        versionId={versionId}
       />
 
       <Row>
diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
index 0a26dbfc5feb7a55a1fd1c4572af780ea6912a56..0c24f66472c8abf9fb6e049472776bb3966699e9 100644
--- a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
+++ b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
@@ -60,7 +60,7 @@ export default compose(
     onSubmit: (
       values,
       dispatch,
-      { inviteReviewer, collectionId, getReviewers, reset },
+      { inviteReviewer, collectionId, versionId, getReviewers, reset },
     ) => {
       const reviewerData = pick(values, [
         'email',
@@ -68,7 +68,7 @@ export default compose(
         'firstName',
         'affiliation',
       ])
-      inviteReviewer(reviewerData, collectionId).then(() => {
+      inviteReviewer(reviewerData, collectionId, versionId).then(() => {
         reset()
         getReviewers()
       })
diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerList.js b/packages/components-faraday/src/components/Reviewers/ReviewerList.js
index 5acbf18c452cd2c26371dec8d12fc88f566f7df0..4290a947cf9b6a4146a811262976a03f732f9ade 100644
--- a/packages/components-faraday/src/components/Reviewers/ReviewerList.js
+++ b/packages/components-faraday/src/components/Reviewers/ReviewerList.js
@@ -93,6 +93,7 @@ export default compose(
   withHandlers({
     showConfirmResend: ({
       showModal,
+      versionId,
       collectionId,
       inviteReviewer,
       goBackToReviewers,
@@ -104,6 +105,7 @@ export default compose(
           inviteReviewer(
             pick(reviewer, ['email', 'firstName', 'lastName', 'affiliation']),
             collectionId,
+            versionId,
           ).then(goBackToReviewers, goBackToReviewers)
         },
         onCancel: goBackToReviewers,
@@ -112,6 +114,7 @@ export default compose(
     showConfirmRevoke: ({
       showModal,
       hideModal,
+      versionId,
       collectionId,
       revokeReviewer,
       goBackToReviewers,
@@ -120,7 +123,7 @@ export default compose(
         title: 'Unassign Reviewer',
         confirmText: 'Unassign',
         onConfirm: () => {
-          revokeReviewer(invitationId, collectionId).then(
+          revokeReviewer(invitationId, collectionId, versionId).then(
             goBackToReviewers,
             goBackToReviewers,
           )
diff --git a/packages/components-faraday/src/index.js b/packages/components-faraday/src/index.js
index 0eb9c3b474d2da621536bec68cbe2541141b9544..b40accf6eaee709b192b849b8ab1290c47363428 100644
--- a/packages/components-faraday/src/index.js
+++ b/packages/components-faraday/src/index.js
@@ -7,7 +7,6 @@ module.exports = {
       editors: () => require('./redux/editors').default,
       files: () => require('./redux/files').default,
       reviewers: () => require('./redux/reviewers').default,
-      recommendations: () => require('./redux/recommendations').default,
     },
   },
 }
diff --git a/packages/components-faraday/src/redux/authors.js b/packages/components-faraday/src/redux/authors.js
index a348f22be32e767f5f356bbae543fc887082dd5d..324cc26adabc74a3ecc2c4d38111903757c27ab3 100644
--- a/packages/components-faraday/src/redux/authors.js
+++ b/packages/components-faraday/src/redux/authors.js
@@ -1,10 +1,7 @@
-import { get, head } from 'lodash'
-import {
-  create,
-  get as apiGet,
-  remove,
-  update,
-} from 'pubsweet-client/src/helpers/api'
+import { get } from 'lodash'
+import { create, remove, get as apiGet } from 'pubsweet-client/src/helpers/api'
+
+import { handleError } from './utils'
 
 // constants
 const REQUEST = 'authors/REQUEST'
@@ -25,33 +22,29 @@ export const authorSuccess = () => ({
   type: SUCCESS,
 })
 
-export const addAuthor = (author, collectionId) => dispatch => {
+export const getAuthors = (collectionId, fragmentId) =>
+  apiGet(`/collections/${collectionId}/fragments/${fragmentId}/users`)
+
+export const addAuthor = (author, collectionId, fragmentId) => dispatch => {
   dispatch(authorRequest())
-  return create(`/collections/${collectionId}/users`, {
+  return create(`/collections/${collectionId}/fragments/${fragmentId}/users`, {
     email: author.email,
     role: 'author',
     ...author,
-  })
+  }).then(author => {
+    dispatch(authorSuccess())
+    return author
+  }, handleError(authorFailure, dispatch))
 }
 
-export const deleteAuthor = (collectionId, userId) => dispatch => {
+export const deleteAuthor = (collectionId, fragmentId, userId) => dispatch => {
   dispatch(authorRequest())
-  return remove(`/collections/${collectionId}/users/${userId}`)
-}
-
-export const editAuthor = (collectionId, userId, body) =>
-  update(`/collections/${collectionId}/users/${userId}`, body)
-
-export const getAuthors = collectionId =>
-  apiGet(`/collections/${collectionId}/users`)
-
-export const getAuthorsTeam = collectionId =>
-  apiGet(`/teams?object.id=${collectionId}&group=author`).then(teams =>
-    head(teams),
+  return remove(
+    `/collections/${collectionId}/fragments/${fragmentId}/users/${userId}`,
   )
-
-export const updateAuthorsTeam = (teamId, body) =>
-  update(`/teams/${teamId}`, body)
+    .then(() => dispatch(authorSuccess()))
+    .catch(handleError(authorFailure, dispatch))
+}
 
 // selectors
 export const getFragmentAuthors = (state, fragmentId) =>
diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js
index 3314f901decb5254b8c5d943163c7b3dbd3517d7..4a3e1f70ff1efd7ef18b9fb4e1191a3e503a5361 100644
--- a/packages/components-faraday/src/redux/recommendations.js
+++ b/packages/components-faraday/src/redux/recommendations.js
@@ -1,154 +1,36 @@
 import { get } from 'lodash'
 import { create, update } from 'pubsweet-client/src/helpers/api'
 
-// #region Constants
-const REQUEST = 'recommendations/REQUEST'
-const ERROR = 'recommendations/ERROR'
-
-const GET_FRAGMENT_SUCCESS = 'GET_FRAGMENT_SUCCESS'
-const GET_RECOMMENDATIONS_SUCCESS = 'recommendations/GET_SUCCESS'
-const CREATE_RECOMMENDATION_SUCCESS = 'recommendations/CREATE_SUCCESS'
-const UPDATE_RECOMMENDATION_SUCCESS = 'recommendations/UPDATE_SUCCESS'
-// #endregion
-
-// #region Action Creators
-export const recommendationsRequest = () => ({
-  type: REQUEST,
-})
-
-export const recommendationsError = error => ({
-  type: ERROR,
-  error,
-})
-
-export const getRecommendationsSuccess = recommendation => ({
-  type: GET_RECOMMENDATIONS_SUCCESS,
-  payload: { recommendation },
-})
-
-export const createRecommendationSuccess = recommendation => ({
-  type: CREATE_RECOMMENDATION_SUCCESS,
-  payload: { recommendation },
-})
-
-export const updateRecommendationSuccess = recommendation => ({
-  type: UPDATE_RECOMMENDATION_SUCCESS,
-  payload: { recommendation },
-})
-// #endregion
-
 // #region Selectors
-export const selectFetching = state =>
-  get(state, 'recommendations.fetching') || false
-export const selectError = state => get(state, 'recommendations.error')
-export const selectRecommendations = state =>
-  get(state, 'recommendations.recommendations') || []
-export const selectEditorialRecommendations = state =>
-  selectRecommendations(state).filter(
+export const selectRecommendations = (state, fragmentId) =>
+  get(state, `fragments.${fragmentId}.recommendations`) || []
+export const selectEditorialRecommendations = (state, fragmentId) =>
+  selectRecommendations(state, fragmentId).filter(
     r => r.recommendationType === 'editorRecommendation' && r.comments,
   )
 // #endregion
 
 // #region Actions
+// error handling and fetching is handled by the autosave reducer
 export const createRecommendation = (
   collId,
   fragId,
   recommendation,
-) => dispatch => {
-  dispatch(recommendationsRequest())
-  return create(
+) => dispatch =>
+  create(
     `/collections/${collId}/fragments/${fragId}/recommendations`,
     recommendation,
-  ).then(
-    r => {
-      dispatch(getRecommendationsSuccess(r))
-      return r
-    },
-    err => {
-      const error = get(err, 'response')
-      if (error) {
-        const errorMessage = get(JSON.parse(error), 'error')
-        dispatch(recommendationsError(errorMessage))
-      }
-      throw err
-    },
   )
-}
 
 export const updateRecommendation = (
   collId,
   fragId,
   recommendation,
-) => dispatch => {
-  dispatch(recommendationsRequest())
-  return update(
+) => dispatch =>
+  update(
     `/collections/${collId}/fragments/${fragId}/recommendations/${
       recommendation.id
     }`,
     recommendation,
-  ).then(
-    r => {
-      dispatch(getRecommendationsSuccess(r))
-      return r
-    },
-    err => {
-      const error = get(err, 'response')
-      if (error) {
-        const errorMessage = get(JSON.parse(error), 'error')
-        dispatch(recommendationsError(errorMessage))
-      }
-      throw err
-    },
   )
-}
-// #endregion
-
-// #region State
-const initialState = {
-  fetching: false,
-  error: null,
-  recommendations: [],
-}
-
-export default (state = initialState, action = {}) => {
-  switch (action.type) {
-    case REQUEST:
-      return {
-        ...state,
-        fetching: true,
-      }
-    case ERROR:
-      return {
-        ...state,
-        fetching: false,
-        error: action.error,
-      }
-    case GET_FRAGMENT_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: get(action, 'fragment.recommendations'),
-      }
-    case GET_RECOMMENDATIONS_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: state.recommendations
-          ? [...state.recommendations, action.payload.recommendation]
-          : action.payload.recommendation,
-      }
-    case UPDATE_RECOMMENDATION_SUCCESS:
-    case CREATE_RECOMMENDATION_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: [action.payload.recommendation],
-      }
-    default:
-      return state
-  }
-}
 // #endregion
diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js
index e21dae2a3fa17e369ffcae12f83dbc01b7b8c7d0..acc31e86480a3d6b826d89d66a4f9d3302db7b43 100644
--- a/packages/components-faraday/src/redux/reviewers.js
+++ b/packages/components-faraday/src/redux/reviewers.js
@@ -81,19 +81,17 @@ export const selectFetchingInvite = state =>
 export const selectFetchingDecision = state =>
   get(state, 'reviewers.fetching.decision') || false
 
-export const selectInvitation = (state, collectionId) => {
+export const selectInvitation = (state, fragmentId) => {
   const currentUser = selectCurrentUser(state)
-  const collection = state.collections.find(c => c.id === collectionId)
-  const invitations = get(collection, 'invitations') || []
+  const invitations = get(state, `fragments.${fragmentId}.invitations`) || []
   return invitations.find(
     i => i.userId === currentUser.id && i.role === 'reviewer' && !i.hasAnswer,
   )
 }
 
-export const currentUserIsReviewer = (state, collectionId) => {
+export const currentUserIsReviewer = (state, fragmentId) => {
   const currentUser = selectCurrentUser(state)
-  const collection = state.collections.find(c => c.id === collectionId)
-  const invitations = get(collection, 'invitations') || []
+  const invitations = get(state, `fragments.${fragmentId}.invitations`) || []
   return !!invitations.find(
     i =>
       i.userId === currentUser.id &&
@@ -103,22 +101,37 @@ export const currentUserIsReviewer = (state, collectionId) => {
   )
 }
 
-export const getCollectionReviewers = collectionId => dispatch => {
+export const getCollectionReviewers = (
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(getReviewersRequest())
-  return apiGet(`/collections/${collectionId}/invitations?role=reviewer`).then(
+  return apiGet(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations?role=reviewer`,
+  ).then(
     r => dispatch(getReviewersSuccess(orderBy(r, orderReviewers))),
-    err => dispatch(getReviewersError(err)),
+    err => {
+      dispatch(getReviewersError(err))
+      throw err
+    },
   )
 }
 // #endregion
 
 // #region Actions - invitations
-export const inviteReviewer = (reviewerData, collectionId) => dispatch => {
+export const inviteReviewer = (
+  reviewerData,
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(inviteRequest())
-  return create(`/collections/${collectionId}/invitations`, {
-    ...reviewerData,
-    role: 'reviewer',
-  }).then(
+  return create(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations`,
+    {
+      ...reviewerData,
+      role: 'reviewer',
+    },
+  ).then(
     () => dispatch(inviteSuccess()),
     err => {
       dispatch(inviteError(get(JSON.parse(err.response), 'error')))
@@ -135,10 +148,14 @@ export const setReviewerPassword = reviewerBody => dispatch => {
   })
 }
 
-export const revokeReviewer = (invitationId, collectionId) => dispatch => {
+export const revokeReviewer = (
+  invitationId,
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(inviteRequest())
   return remove(
-    `/collections/${collectionId}/invitations/${invitationId}`,
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`,
   ).then(
     () => dispatch(inviteSuccess()),
     err => {
@@ -153,12 +170,16 @@ export const revokeReviewer = (invitationId, collectionId) => dispatch => {
 export const reviewerDecision = (
   invitationId,
   collectionId,
+  fragmentId,
   agree = true,
 ) => dispatch => {
   dispatch(reviewerDecisionRequest())
-  return update(`/collections/${collectionId}/invitations/${invitationId}`, {
-    isAccepted: agree,
-  }).then(
+  return update(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`,
+    {
+      isAccepted: agree,
+    },
+  ).then(
     res => {
       dispatch(reviewerDecisionSuccess())
       return res
diff --git a/packages/components-faraday/src/redux/utils.js b/packages/components-faraday/src/redux/utils.js
index 14560f9260f6bfeec3f768e7a47b2c2147c1d111..a84e7328c7de7431436d9cc8fc10bf6551e0f119 100644
--- a/packages/components-faraday/src/redux/utils.js
+++ b/packages/components-faraday/src/redux/utils.js
@@ -9,3 +9,12 @@ export const orderReviewers = r => {
       return 1
   }
 }
+
+export const handleError = (fn, dispatch = null) => err => {
+  if (typeof dispatch === 'function') {
+    dispatch(fn(err))
+  } else {
+    fn(err)
+  }
+  throw err
+}
diff --git a/packages/xpub-faraday/app/config/journal/submit-wizard.js b/packages/xpub-faraday/app/config/journal/submit-wizard.js
index 1dd84548c68bbf86c623db2e2fa95bb7da29cf92..a7c54767357c09f25fe2d67e4e7084471749ad02 100644
--- a/packages/xpub-faraday/app/config/journal/submit-wizard.js
+++ b/packages/xpub-faraday/app/config/journal/submit-wizard.js
@@ -10,10 +10,10 @@ import { declarations } from './'
 import issueTypes from './issues-types'
 import manuscriptTypes from './manuscript-types'
 import {
-  requiredBasedOnType,
-  editModeEnabled,
-  parseEmptyHtml,
   requiredFiles,
+  parseEmptyHtml,
+  editModeEnabled,
+  requiredBasedOnType,
 } from './wizard-validators'
 
 const min3Chars = minChars(3)
@@ -51,7 +51,13 @@ const uploadFile = input => uploadFileFn(input)
 
 export default {
   showProgress: true,
-  formSectionKeys: ['metadata', 'declarations', 'conflicts', 'files'],
+  formSectionKeys: [
+    'authors',
+    'metadata',
+    'declarations',
+    'conflicts',
+    'files',
+  ],
   submissionRedirect: '/confirmation-page',
   dispatchFunctions: [uploadFile],
   steps: [
@@ -147,7 +153,7 @@ export default {
           validate: [required],
         },
         {
-          fieldId: 'editMode',
+          fieldId: 'authorForm',
           renderComponent: Spacing,
           validate: [editModeEnabled],
         },
diff --git a/packages/xpub-faraday/app/config/journal/wizard-validators.js b/packages/xpub-faraday/app/config/journal/wizard-validators.js
index bec3371d1699e67c3ef81688f26205bee7328b46..25d58d4372c51981864e9e7822e81b219ac9123e 100644
--- a/packages/xpub-faraday/app/config/journal/wizard-validators.js
+++ b/packages/xpub-faraday/app/config/journal/wizard-validators.js
@@ -23,7 +23,7 @@ export const requiredBasedOnType = (value, formValues) => {
   return undefined
 }
 
-export const editModeEnabled = value => {
+export const editModeEnabled = (value, allValues) => {
   if (value) {
     return 'You have some unsaved author details.'
   }
diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js
index 1b7642bfca5445c4c1651d8cbfcca8aad45ef8c5..ad75355dd14fd8f26a70e2f75bb2f8c74a87054d 100644
--- a/packages/xpub-faraday/config/authsome-helpers.js
+++ b/packages/xpub-faraday/config/authsome-helpers.js
@@ -1,6 +1,6 @@
-const omit = require('lodash/omit')
+const { omit, get, last } = require('lodash')
+
 const config = require('config')
-const get = require('lodash/get')
 
 const statuses = config.get('statuses')
 
@@ -44,7 +44,10 @@ const filterObjectData = (
           rec => rec.userId === user.id,
         )
     }
-
+    parseAuthorsData(object, matchingCollPerm)
+    if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
+      return filterRefusedInvitations(object, user)
+    }
     return object
   }
   const matchingCollPerm = collectionsPermissions.find(
@@ -52,16 +55,16 @@ const filterObjectData = (
   )
   if (matchingCollPerm === undefined) return null
   setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
 
   return object
 }
 
-const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
-  const teams = await Promise.all(
+const getTeamsByPermissions = async (
+  teamIds = [],
+  permissions = [],
+  TeamModel,
+) =>
+  (await Promise.all(
     teamIds.map(async teamId => {
       const team = await TeamModel.find(teamId)
       if (!permissions.includes(team.teamType.permissions)) {
@@ -69,15 +72,69 @@ const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
       }
       return team
     }),
+  )).filter(Boolean)
+
+const heIsInvitedToFragment = async ({ user, Team, collectionId }) =>
+  (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some(
+    // user is a member of the team with access to the fragment's parent collection
+    t => t.members.includes(user.id) && t.object.id === collectionId,
   )
 
-  return teams.filter(Boolean)
+const getUserPermissions = async ({
+  user,
+  Team,
+  mapFn = t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }),
+}) =>
+  (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn)
+
+const isOwner = ({ user: { id }, object }) => {
+  if (object.owners.includes(id)) return true
+  return !!object.owners.find(own => own.id === id)
+}
+
+const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => {
+  const userPermissions = await getUserPermissions({
+    user,
+    Team,
+  })
+
+  return !!userPermissions.find(p => {
+    const hasObject =
+      p.objectId === get(object, 'fragment.id') ||
+      p.objectId === get(object, 'fragment.collectionId')
+    if (roles.length > 0) {
+      return hasObject && roles.includes(p.role)
+    }
+    return hasObject
+  })
+}
+
+const isHandlingEditor = ({ user, object }) =>
+  get(object, 'collection.handlingEditor.id') === user.id
+
+const isInDraft = fragment => !get(fragment, 'submitted')
+
+const hasFragmentInDraft = async ({ object, Fragment }) => {
+  const lastFragmentId = last(get(object, 'fragments'))
+  const fragment = await Fragment.find(lastFragmentId)
+  return isInDraft(fragment)
 }
 
 module.exports = {
+  filterObjectData,
   parseAuthorsData,
   setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
   getTeamsByPermissions,
+  filterRefusedInvitations,
+  isOwner,
+  isHandlingEditor,
+  getUserPermissions,
+  heIsInvitedToFragment,
+  hasPermissionForObject,
+  isInDraft,
+  hasFragmentInDraft,
 }
diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js
index 948dd93e25476745ef93e4db143ff99f79ff8cec..65787b60bf24b3158cc86e51d4613e388766f67a 100644
--- a/packages/xpub-faraday/config/authsome-mode.js
+++ b/packages/xpub-faraday/config/authsome-mode.js
@@ -1,62 +1,8 @@
-const get = require('lodash/get')
-const pickBy = require('lodash/pickBy')
-const omit = require('lodash/omit')
-const helpers = require('./authsome-helpers')
-
-async function teamPermissions(user, operation, object, context) {
-  const permissions = ['handlingEditor', 'author', 'reviewer']
-  const teams = await helpers.getTeamsByPermissions(
-    user.teams,
-    permissions,
-    context.models.Team,
-  )
-
-  let collectionsPermissions = await Promise.all(
-    teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
-      if (
-        collection.status === 'rejected' &&
-        team.teamType.permissions === 'reviewer'
-      )
-        return null
-      const collPerm = {
-        id: collection.id,
-        permission: team.teamType.permissions,
-      }
-      const objectType = get(object, 'type')
-      if (objectType === 'fragment') {
-        if (collection.fragments.includes(object.id))
-          collPerm.fragmentId = object.id
-        else return null
-      }
+const config = require('config')
+const { get, pickBy, omit } = require('lodash')
 
-      if (objectType === 'collection')
-        if (object.id !== collection.id) return null
-      return collPerm
-    }),
-  )
-  collectionsPermissions = collectionsPermissions.filter(cp => cp !== null)
-  if (collectionsPermissions.length === 0) return false
-
-  return {
-    filter: filterParam => {
-      if (!filterParam.length) {
-        return helpers.filterObjectData(
-          collectionsPermissions,
-          filterParam,
-          user,
-        )
-      }
-
-      const collections = filterParam
-        .map(coll =>
-          helpers.filterObjectData(collectionsPermissions, coll, user),
-        )
-        .filter(Boolean)
-      return collections
-    },
-  }
-}
+const statuses = config.get('statuses')
+const helpers = require('./authsome-helpers')
 
 function unauthenticatedUser(operation, object) {
   // Public/unauthenticated users can GET /collections, filtered by 'published'
@@ -104,131 +50,219 @@ function unauthenticatedUser(operation, object) {
   return false
 }
 
+const publicStatusesPermissions = ['author', 'reviewer']
+const createPaths = ['/collections', '/collections/:collectionId/fragments']
+
 async function authenticatedUser(user, operation, object, context) {
-  // Allow the authenticated user to POST a collection (but not with a 'filtered' property)
-  if (operation === 'POST' && object.path === '/collections') {
-    return {
-      filter: collection => omit(collection, 'filtered'),
+  if (operation === 'GET') {
+    if (get(object, 'path') === '/collections') {
+      return {
+        filter: async collections => {
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          return collections.filter(collection => {
+            if (collection.owners.includes(user.id)) {
+              return true
+            }
+            const collectionPermission = userPermissions.find(
+              p => p.objectId === collection.id,
+            )
+            if (collectionPermission) {
+              return true
+            }
+
+            const fragmentPermission = userPermissions.find(p =>
+              collection.fragments.includes(p.objectId),
+            )
+            if (fragmentPermission) {
+              return true
+            }
+            return false
+          })
+        },
+      }
     }
-  }
 
-  if (
-    operation === 'POST' &&
-    object.path === '/collections/:collectionId/fragments'
-  ) {
-    return true
-  }
+    if (object === '/users') {
+      return true
+    }
 
-  // allow authenticate owners full pass for a collection
-  if (get(object, 'type') === 'collection') {
-    if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
+      if (helpers.isOwner({ user, object })) {
+        if (
+          await helpers.hasFragmentInDraft({
+            object,
+            Fragment: context.models.Fragment,
+          })
+        ) {
+          return {
+            filter: collection => ({
+              ...collection,
+              status: 'draft',
+              visibleStatus: statuses.draft.public,
+            }),
+          }
+        }
+        return true
+      }
       return {
-        filter: collection => omit(collection, 'filtered'),
+        filter: async collection => {
+          const status = get(collection, 'status') || 'draft'
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          if (collection.owners.map(o => o.id).includes(user.id)) {
+            return collection
+          }
+
+          const collectionPermission = userPermissions.find(
+            p => p.objectId === collection.id,
+          )
+          if (
+            publicStatusesPermissions.includes(
+              get(collectionPermission, 'role'),
+            )
+          ) {
+            collection.visibleStatus = statuses[status].public
+          }
+          return collection
+        },
       }
     }
-    if (object.owners.includes(user.id)) return true
-    const owner = object.owners.find(own => own.id === user.id)
-    if (owner !== undefined) return true
-  }
 
-  // Allow owners of a collection to GET its teams, e.g.
-  // GET /api/collections/1/teams
-  if (operation === 'GET' && get(object, 'path') === '/teams') {
-    const collectionId = get(object, 'params.collectionId')
-    if (collectionId) {
-      const collection = await context.models.Collection.find(collectionId)
-      if (collection.owners.includes(user.id)) {
+    if (get(object, 'type') === 'fragment') {
+      if (helpers.isOwner({ user, object })) {
         return true
       }
+
+      if (helpers.isInDraft(object)) {
+        return false
+      }
+
+      const userPermissions = await helpers.getUserPermissions({
+        user,
+        Team: context.models.Team,
+      })
+
+      const permission = userPermissions.find(
+        p => p.objectId === object.id || p.objectId === object.collectionId,
+      )
+
+      if (!permission) return false
+
+      return {
+        filter: fragment => {
+          // handle other roles
+          if (permission.role === 'reviewer') {
+            fragment.files = omit(fragment.files, ['coverLetter'])
+            fragment.authors = fragment.authors.map(a => omit(a, ['email']))
+            fragment.recommendations = fragment.recommendations
+              ? fragment.recommendations.filter(r => r.userId === user.id)
+              : []
+          }
+          return fragment
+        },
+      }
     }
-  }
 
-  if (
-    operation === 'GET' &&
-    get(object, 'type') === 'team' &&
-    get(object, 'object.type') === 'collection'
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object, 'object.id'),
-    )
-    if (collection.owners.includes(user.id)) {
+    if (get(object, 'type') === 'user') {
       return true
     }
-  }
 
-  // Advanced example
-  // Allow authenticated users to create a team based around a collection
-  // if they are one of the owners of this collection
-  if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') {
-    if (get(object, 'object.type') === 'collection') {
-      const collection = await context.models.Collection.find(
-        get(object, 'object.id'),
-      )
-      if (collection.owners.includes(user.id)) {
+    // allow HE to get reviewer invitations
+    if (get(object, 'fragment.type') === 'fragment') {
+      const collectionId = get(object, 'fragment.collectionId')
+      const collection = await context.models.Collection.find(collectionId)
+
+      if (get(collection, 'handlingEditor.id') === user.id) {
         return true
       }
     }
-  }
-
-  // only allow the HE to create, delete an invitation, or get invitation details
-  if (
-    ['POST', 'GET', 'DELETE'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('invitations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const handlingEditor = get(collection, 'handlingEditor')
-    if (!handlingEditor) return false
-    if (handlingEditor.id === user.id) return true
-    return false
-  }
 
-  // only allow a reviewer and an HE to submit and to modify a recommendation
-  if (
-    ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('recommendations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const teams = await helpers.getTeamsByPermissions(
-      user.teams,
-      ['reviewer', 'handlingEditor'],
-      context.models.Team,
-    )
-    if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
-    if (matchingTeam) return true
-    return false
+    if (get(object, 'type') === 'user') {
+      return true
+    }
   }
 
-  if (user.teams.length !== 0 && ['GET'].includes(operation)) {
-    const permissions = await teamPermissions(user, operation, object, context)
+  if (operation === 'POST') {
+    // allow everytone to create manuscripts and versions
+    if (createPaths.includes(object.path)) {
+      return true
+    }
 
-    if (permissions) {
-      return permissions
+    // allow HE to invite
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
     }
 
-    return false
+    // allow HE or assigned reviewers to recommend
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer', 'handlingEditor'],
+      })
+    }
   }
 
-  if (get(object, 'type') === 'fragment') {
-    const fragment = object
+  if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
+    }
+
+    if (get(object, 'type') === 'fragment') {
+      return helpers.isOwner({ user, object })
+    }
+
+    // allow reviewer to patch his recommendation
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer'],
+      })
+    }
 
-    if (fragment.owners.includes(user.id)) {
+    if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
       return true
     }
+
+    // allow owner to submit a revision
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/submit'
+    ) {
+      return helpers.isOwner({ user, object: object.fragment })
+    }
   }
 
-  // A user can GET, DELETE and PATCH itself
-  if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
-    if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
-      return true
+  if (operation === 'DELETE') {
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
+
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
     }
   }
+
   // If no individual permissions exist (above), fallback to unauthenticated
   // user's permission
   return unauthenticatedUser(operation, object)
@@ -244,7 +278,7 @@ const authsomeMode = async (userId, operation, object, context) => {
   const user = await context.models.User.find(userId)
 
   // Admins and editor in chiefs can do anything
-  if (user && (user.admin || user.editorInChief)) return true
+  // if (user && (user.admin || user.editorInChief)) return true
 
   if (user) {
     return authenticatedUser(user, operation, object, context)
diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js
index ec5a914c3268276d718abf8d87a78aeba7c7909d..b8d8d607061cd81a82c828686002baac69a89312 100644
--- a/packages/xpub-faraday/config/validations.js
+++ b/packages/xpub-faraday/config/validations.js
@@ -7,9 +7,7 @@ module.exports = {
     created: Joi.date(),
     title: Joi.string(),
     status: Joi.string(),
-    reviewers: Joi.array(),
     customId: Joi.string(),
-    authors: Joi.array(),
     invitations: Joi.array(),
     handlingEditor: Joi.object(),
     visibleStatus: Joi.string().allow(''),
@@ -17,6 +15,7 @@ module.exports = {
   fragment: [
     {
       fragmentType: Joi.valid('version').required(),
+      collectionId: Joi.string().required(),
       created: Joi.date(),
       version: Joi.number(),
       submitted: Joi.date(),
@@ -73,21 +72,8 @@ module.exports = {
       reviewers: Joi.array(),
       lock: Joi.object(),
       decision: Joi.object(),
-      authors: Joi.array().items(
-        Joi.object({
-          firstName: Joi.string().required(),
-          lastName: Joi.string().required(),
-          middleName: Joi.string().allow(''),
-          email: Joi.string()
-            .email()
-            .required(),
-          affiliation: Joi.string().required(),
-          country: Joi.string().allow(''),
-          isSubmitting: Joi.boolean(),
-          isCorresponding: Joi.boolean(),
-          id: Joi.string().uuid(),
-        }),
-      ),
+      authors: Joi.array(),
+      invitations: Joi.array(),
       recommendations: Joi.array().items(
         Joi.object({
           id: Joi.string().required(),
diff --git a/packages/xpub-faraday/package.json b/packages/xpub-faraday/package.json
index af02408c9615982e86f1986419b55fa747fe99bd..04202a73693802c91fbd6b50deb6d3f3be345eda 100644
--- a/packages/xpub-faraday/package.json
+++ b/packages/xpub-faraday/package.json
@@ -36,7 +36,7 @@
     "react-router-dom": "^4.2.2",
     "recompose": "^0.26.0",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "redux-logger": "^3.0.1",
     "typeface-noto-sans": "^0.0.54",
     "typeface-noto-serif": "^0.0.54",
diff --git a/yarn.lock b/yarn.lock
index 13f78b9bf62e6523df4c8851f5ebbc1984b7a6b5..b164542ac0453b3b4f320ee8df9784014cbda154 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3325,7 +3325,7 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
     es6-iterator "~2.0.3"
     es6-symbol "~3.1.1"
 
-es6-error@^4.1.1:
+es6-error@^4.0.0, es6-error@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
 
@@ -4512,6 +4512,10 @@ hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.3.0, hoist-non-react-
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
 
+hoist-non-react-statics@^2.2.1:
+  version "2.5.4"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f"
+
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -8311,6 +8315,19 @@ reduce-function-call@^1.0.1:
   dependencies:
     balanced-match "^0.4.2"
 
+redux-form@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.0.3.tgz#80157d01df7de6c8eb2297ad1fbbb092bafa34f5"
+  dependencies:
+    deep-equal "^1.0.1"
+    es6-error "^4.0.0"
+    hoist-non-react-statics "^2.2.1"
+    invariant "^2.2.2"
+    is-promise "^2.1.0"
+    lodash "^4.17.3"
+    lodash-es "^4.17.3"
+    prop-types "^15.5.9"
+
 redux-form@^7.0.3:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.2.3.tgz#a01111116f386f3d88451b5528dfbb180561a8b4"