diff --git a/.gitlab/issue_templates/Feature Request.md b/.gitlab/issue_templates/Feature Request.md
new file mode 100644
index 0000000000000000000000000000000000000000..7e4c7cad02e277e3357f8cf7f1a6bacea2531b67
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Request.md	
@@ -0,0 +1,27 @@
+[Template for request for comments (RFC) on feature proposals]
+
+# RFC: Feature proposal: [Title]
+
+## Context
+
+[Give the necessary context for your proposal. For example, what problem will this feature solve for users? What are the use cases, benefits, and goals?]
+
+## Proposal
+
+[A precise statement of the proposed feature.]
+
+## Design
+
+[Include sketch or wireframes of the UI changes necessary for this feature]
+
+## Implementation (if applicable)
+
+[A description of the steps to implement the feature.]
+
+## Alternative approaches (if applicable)
+
+[Include any alternatives to meet this use case.]
+
+## Open issues (if applicable)
+
+[Links to and a discussion of related issues, if applicable.]
diff --git a/babel.config.js b/babel.config.js
index 4dba60f1ad5d4cc35155b02504d07ddb7276a7d2..11b59c343cb1d1bbe0f492ed84d8e862298f0dc9 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -2,6 +2,6 @@ module.exports = {
   presets: ['@babel/preset-env', '@babel/preset-react'],
   plugins: [
     'babel-plugin-styled-components',
-    '@babel/plugin-proposal-class-properties',
+    ['@babel/plugin-proposal-class-properties', { loose: true }],
   ],
 }
diff --git a/config/default.js b/config/default.js
index 2d981ecd29abd4fdb407caf71a9293e07334adff..f6f93f7ed11e9e7d0d9bc2bc71381b355c94bcf5 100644
--- a/config/default.js
+++ b/config/default.js
@@ -140,7 +140,8 @@ module.exports = {
     theme: process.env.PUBSWEET_THEME,
     baseUrl: deferConfig(cfg => {
       const { protocol, host, port } = cfg['pubsweet-client']
-      return `${protocol}://${host}${port ? `:${port}` : ''}`
+      const hostname = host === '0.0.0.0' ? 'localhost' : host
+      return `${protocol}://${hostname}${port ? `:${port}` : ''}`
     }),
   },
   'pubsweet-component-xpub-dashboard': {
diff --git a/config/permissions.js b/config/permissions.js
index c377a7c20e4efc4a88a046ef803febac7bd39d50..f51de9a0dee1dd7c1486a0d4f66a1cd555228351 100644
--- a/config/permissions.js
+++ b/config/permissions.js
@@ -1,7 +1,7 @@
 // eslint-disable-next-line no-unused-vars
 const { rule, shield, and, or, not, allow, deny } = require('graphql-shield')
 
-const _userIsEditor = async (user, manuscriptId) => {
+const userIsEditorQuery = async (user, manuscriptId) => {
   if (!user) {
     return false
   }
@@ -25,9 +25,9 @@ const _userIsEditor = async (user, manuscriptId) => {
 
 const userIsEditor = rule({
   cache: 'contextual',
-})(async (parent, args, ctx, info) => _userIsEditor(ctx.user))
+})(async (parent, args, ctx, info) => userIsEditorQuery(ctx.user))
 
-const _userIsMemberOfTeamWithRole = async (user, manuscriptId, role) => {
+const userIsMemberOfTeamWithRoleQuery = async (user, manuscriptId, role) => {
   if (!user) {
     return false
   }
@@ -36,6 +36,7 @@ const _userIsMemberOfTeamWithRole = async (user, manuscriptId, role) => {
     .$relatedQuery('teams')
     .where({ role })
     .andWhere({ manuscriptId })
+
   const rows = await query.resultSize()
   return !!rows
 }
@@ -44,27 +45,26 @@ const userIsAdmin = rule({ cache: 'contextual' })(
   async (parent, args, ctx, info) => ctx.user && ctx.user.admin,
 )
 
-const parent_manuscript_is_published = rule({ cache: 'contextual' })(
+const parentManuscriptIsPublished = rule({ cache: 'contextual' })(
   async (parent, args, ctx, info) => {
     const manuscript = await ctx.models.Manuscript.query().findById(
       parent.manuscriptId,
     )
+
     return !!manuscript.published
   },
 )
 
-const review_is_by_user = rule({ cache: 'contextual' })(
+const reviewIsByUser = rule({ cache: 'contextual' })(
   async (parent, args, ctx, info) => {
     const rows =
       ctx.user &&
-      ctx.user
-        .$relatedQuery('teams')
-        .where({ role: 'reviewer' })
-        .resultSize()
+      ctx.user.$relatedQuery('teams').where({ role: 'reviewer' }).resultSize()
 
     return !!rows
   },
 )
+
 const isAuthenticated = rule({ cache: 'contextual' })(
   async (parent, args, ctx, info) => !!ctx.user,
 )
@@ -78,35 +78,44 @@ const userIsAllowedToChat = rule({ cache: 'strict' })(
     if (ctx.user && ctx.user.admin) {
       return true
     }
+
     const channel = await ctx.models.Channel.query().findById(args.channelId)
+
     const manuscript = await ctx.models.Manuscript.query().findById(
       channel.manuscriptId,
     )
 
-    const isAuthor = await _userIsMemberOfTeamWithRole(
+    const isAuthor = await userIsMemberOfTeamWithRoleQuery(
       ctx.user,
       manuscript.id,
       'author',
     )
-    const isReviewer = await _userIsMemberOfTeamWithRole(
+
+    const isReviewer = await userIsMemberOfTeamWithRoleQuery(
       ctx.user,
       manuscript.id,
       'reviewer',
     )
-    const isEditor = await _userIsEditor(ctx.user, manuscript.id)
+
+    const isEditor = await userIsEditorQuery(ctx.user, manuscript.id)
 
     if (channel.type === 'all') {
       return isAuthor || isReviewer || isEditor
-    } else if (channel.type === 'editorial') {
+    }
+
+    if (channel.type === 'editorial') {
       return isReviewer || isEditor
     }
+
+    return false
   },
 )
 
-const user_is_review_author_and_review_is_not_completed = rule({
+const userIsReviewAuthorAndReviewIsNotCompleted = rule({
   cache: 'strict',
 })(async (parent, args, ctx, info) => {
   let manuscriptId
+
   if (args.id) {
     ;({ manuscriptId } = await ctx.models.Review.query().findById(args.id))
   } else {
@@ -114,12 +123,14 @@ const user_is_review_author_and_review_is_not_completed = rule({
   }
 
   const manuscript = await ctx.models.Manuscript.query().findById(manuscriptId)
+
   const team = await ctx.models.Team.query()
     .where({
       manuscriptId: manuscript.id,
       role: 'reviewer',
     })
     .first()
+
   if (!team) return false
 
   const members = await team
@@ -133,22 +144,24 @@ const user_is_review_author_and_review_is_not_completed = rule({
   return false
 })
 
-const user_is_editor_of_the_manuscript_of_the_review = rule({
+const userIsEditorOfTheManuscriptOfTheReview = rule({
   cache: 'strict',
 })(async (parent, args, ctx, info) => {
   let manuscriptId
+
   if (args.id) {
     ;({ manuscriptId } = await ctx.models.Review.query().findById(args.id))
   } else {
     ;({ manuscriptId } = args.input)
   }
 
-  return _userIsEditor(ctx.user, manuscriptId)
+  return userIsEditorQuery(ctx.user, manuscriptId)
 })
 
-const user_is_invited_reviewer = rule({ cache: 'strict' })(
+const userIsInvitedReviewer = rule({ cache: 'strict' })(
   async (parent, args, ctx, info) => {
     const team = await ctx.models.Team.query().findById(args.teamId)
+
     const member = await team
       .$relatedQuery('members')
       .where({ userId: ctx.user.id, status: 'invited' })
@@ -158,7 +171,7 @@ const user_is_invited_reviewer = rule({ cache: 'strict' })(
   },
 )
 
-const user_is_author = rule({ cache: 'strict' })(
+const userIsAuthor = rule({ cache: 'strict' })(
   async (parent, args, ctx, info) => {
     const team = await ctx.models.Team.query()
       .where({
@@ -166,6 +179,7 @@ const user_is_author = rule({ cache: 'strict' })(
         role: 'author',
       })
       .first()
+
     const author = team
       .$relatedQuery('members')
       .where({ userId: ctx.user.id })
@@ -175,10 +189,11 @@ const user_is_author = rule({ cache: 'strict' })(
   },
 )
 
-const user_is_author_of_files_associated_manuscript = rule({
+const userIsAuthorOfFilesAssociatedManuscript = rule({
   cache: 'no_cache',
 })(async (parent, args, ctx, info) => {
   let manuscriptId
+
   if (args.meta && args.meta.manuscriptId) {
     // Meta is supplied for createFile
     // eslint-disable-next-line prefer-destructuring
@@ -202,6 +217,7 @@ const user_is_author_of_files_associated_manuscript = rule({
   if (!team) {
     return false
   }
+
   const members = await team
     .$relatedQuery('members')
     .where('userId', ctx.user.id)
@@ -212,8 +228,13 @@ const user_is_author_of_files_associated_manuscript = rule({
 
   return false
 })
-const user_is_author_of_the_manuscript_of_the_file = rule({ cache: 'strict' })(
+
+const userIsAuthorOfTheManuscriptOfTheFile = rule({ cache: 'strict' })(
   async (parent, args, ctx, info) => {
+    if (!ctx.user) {
+      return false
+    }
+
     const manuscript = await ctx.models.File.relatedQuery('manuscript')
       .for(parent.id)
       .first()
@@ -228,6 +249,7 @@ const user_is_author_of_the_manuscript_of_the_file = rule({ cache: 'strict' })(
     if (!team) {
       return false
     }
+
     const members = await team
       .$relatedQuery('members')
       .where('userId', ctx.user.id)
@@ -241,11 +263,13 @@ const user_is_author_of_the_manuscript_of_the_file = rule({ cache: 'strict' })(
 )
 
 // ¯\_(ツ)_/¯
-const user_is_the_reviewer_of_the_manuscript_of_the_file_and_review_not_complete = rule(
-  {
-    cache: 'strict',
-  },
-)(async (parent, args, ctx, info) => {
+const userIsTheReviewerOfTheManuscriptOfTheFileAndReviewNotComplete = rule({
+  cache: 'strict',
+})(async (parent, args, ctx, info) => {
+  if (!ctx.user) {
+    return false
+  }
+
   const manuscript = await ctx.models.File.relatedQuery('manuscript')
     .for(parent.id)
     .first()
@@ -260,6 +284,7 @@ const user_is_the_reviewer_of_the_manuscript_of_the_file_and_review_not_complete
   if (!team) {
     return false
   }
+
   const members = await team
     .$relatedQuery('members')
     .where('userId', ctx.user.id)
@@ -286,21 +311,21 @@ const permissions = {
   Mutation: {
     upload: isAuthenticated,
     createManuscript: isAuthenticated,
-    updateManuscript: user_is_author,
-    submitManuscript: user_is_author,
+    updateManuscript: userIsAuthor,
+    submitManuscript: userIsAuthor,
     createMessage: userIsAllowedToChat,
     updateReview: or(
-      user_is_review_author_and_review_is_not_completed,
-      user_is_editor_of_the_manuscript_of_the_review,
+      userIsReviewAuthorAndReviewIsNotCompleted,
+      userIsEditorOfTheManuscriptOfTheReview,
     ),
-    reviewerResponse: user_is_invited_reviewer,
+    reviewerResponse: userIsInvitedReviewer,
     completeReview: or(
-      user_is_review_author_and_review_is_not_completed,
-      user_is_editor_of_the_manuscript_of_the_review,
+      userIsReviewAuthorAndReviewIsNotCompleted,
+      userIsEditorOfTheManuscriptOfTheReview,
     ),
     createNewVersion: allow,
-    createFile: user_is_author_of_files_associated_manuscript,
-    deleteFile: user_is_author_of_files_associated_manuscript,
+    createFile: userIsAuthorOfFilesAssociatedManuscript,
+    deleteFile: userIsAuthorOfFilesAssociatedManuscript,
   },
   Subscription: {
     messageCreated: userIsAllowedToChat,
@@ -314,16 +339,16 @@ const permissions = {
   Manuscript: allow,
   ManuscriptVersion: allow,
   File: or(
-    parent_manuscript_is_published,
+    parentManuscriptIsPublished,
     or(
-      user_is_author_of_the_manuscript_of_the_file,
-      user_is_the_reviewer_of_the_manuscript_of_the_file_and_review_not_complete,
+      userIsAuthorOfTheManuscriptOfTheFile,
+      userIsTheReviewerOfTheManuscriptOfTheFileAndReviewNotComplete,
       userIsEditor,
       userIsAdmin,
     ),
   ),
   UploadResult: allow,
-  Review: or(parent_manuscript_is_published, review_is_by_user),
+  Review: or(parentManuscriptIsPublished, reviewIsByUser),
   ReviewComment: allow,
   Channel: allow,
   Message: allow,
@@ -337,10 +362,11 @@ const permissions = {
 const fallbackRule = or(userIsAdmin, userIsEditor)
 
 // We only ever need to go two levels down, so no need for recursion
-const addOverrideRule = permissions => {
+const addOverrideRule = perms => {
   const adaptedPermissions = {}
-  Object.keys(permissions).forEach(key1 => {
-    const value = permissions[key1]
+  Object.keys(perms).forEach(key1 => {
+    const value = perms[key1]
+
     if (value.constructor.name !== 'Object') {
       adaptedPermissions[key1] = or(fallbackRule, value)
     } else {
diff --git a/docker-compose.yml b/docker-compose.yml
index bdbbd9db199bacd236d0f5bff34de2051b70bea5..661d365e11a8ce522d6e832cc457cf033693db94 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,10 +14,11 @@ services:
         './webpack/webpack.development.config.js',
       ]
     ports:
-      - ${CLIENT_PORT:-4000}:8080
+      - ${CLIENT_PORT:-4000}:${CLIENT_PORT:-4000}
     environment:
       - NODE_ENV=development
       - CLIENT_HOST=0.0.0.0
+      - CLIENT_PORT=${CLIENT_PORT:-4000}
       - SERVER_PROTOCOL=http
       - SERVER_HOST=server
       - SERVER_PORT=3000
diff --git a/stories/sth/FakeButton.js b/stories/sth/FakeButton.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2dc5e392c1604b79951bff92fa1b8d9c6f5af08
--- /dev/null
+++ b/stories/sth/FakeButton.js
@@ -0,0 +1,24 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styled from 'styled-components'
+
+const Btn = styled.button`
+  background-color: ${props => props.primary && 'blue'};
+  font-family: ${props => props.theme.fontInterface};
+`
+
+/** This appears in storybook */
+const Button = ({ label, primary }) => <Btn primary={primary}>{label}</Btn>
+
+Button.propTypes = {
+  /** This also appears in storybook */
+  label: PropTypes.string.isRequired,
+  /** As props table documentation */
+  primary: PropTypes.bool,
+}
+
+Button.defaultProps = {
+  primary: false,
+}
+
+export default Button
diff --git a/stories/sth/FakeButton.stories.js b/stories/sth/FakeButton.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..83726b307c7a134175f11396b01422e381749757
--- /dev/null
+++ b/stories/sth/FakeButton.stories.js
@@ -0,0 +1,22 @@
+import React from 'react'
+
+import Button from './FakeButton'
+
+export const Base = args => <Button {...args} />
+
+Base.args = {
+  label: 'Click me',
+  primary: false,
+}
+
+export const Primary = () => <Button label="Primary" primary />
+export const NonPrimary = () => <Button label="Not primary" />
+
+export default {
+  title: 'Fake Section/Button',
+  component: Button,
+  argTypes: {
+    label: { control: { type: 'text' } },
+    primary: { control: { type: 'boolean' } },
+  },
+}