diff --git a/packages/server/config/test.js b/packages/server/config/test.js
index 93143c4ee1e7f833d082d3b9dba44cb4125c62cc..e60429ba4eaf4cf0d47e8d9a86941b288b5b37db 100644
--- a/packages/server/config/test.js
+++ b/packages/server/config/test.js
@@ -4,6 +4,7 @@ const winston = require('winston')
 module.exports = {
   'pubsweet-server': {
     logger: new winston.Logger({ level: 'warn' }),
+    port: 4000,
     secret: 'test',
     sse: false,
   },
diff --git a/packages/server/package.json b/packages/server/package.json
index f00bae033ff9e5d4c9d8b2c5223f6de30503e523..393c8cfe8de7e9b7b33976179270d895563f4867 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -15,6 +15,7 @@
   "main": "src/index.js",
   "dependencies": {
     "@pubsweet/logger": "^0.2.1",
+    "apollo-server-express": "^1.3.2",
     "authsome": "0.0.9",
     "bcrypt": "^1.0.2",
     "bluebird": "^3.5.1",
@@ -24,6 +25,8 @@
     "cookie-parser": "^1.4.3",
     "dotenv": "^4.0.0",
     "express": "^4.16.1",
+    "graphql": "^0.12.3",
+    "graphql-tools": "^2.18.0",
     "helmet": "^3.8.1",
     "http-status-codes": "^1.0.6",
     "joi": "^13.1.0",
diff --git a/packages/server/src/connectors/creators.js b/packages/server/src/connectors/creators.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ec3e5281438e2b8c20e9b6337748bba71f10782
--- /dev/null
+++ b/packages/server/src/connectors/creators.js
@@ -0,0 +1,120 @@
+const authsome = require('../helpers/authsome')
+const AuthorizationError = require('../errors/AuthorizationError')
+const NotFoundError = require('../errors/NotFoundError')
+
+// check permissions or throw authroization error
+async function can(userId, verb, entity) {
+  const permission = await authsome.can(userId, verb, entity)
+  if (!permission) {
+    throw new AuthorizationError(
+      `Operation not permitted: ${verb} ${entity.type || entity}`,
+    )
+  }
+  // return identity if no filter function
+  return permission.filter || (id => id)
+}
+
+// check 'read' permissions or throw not found error (to avoid leaking the existence of data)
+async function canKnowAbout(userId, entity) {
+  const permission = await authsome.can(userId, 'read', entity)
+  if (!permission) {
+    throw new NotFoundError(
+      `Object not found: ${entity.type} with id ${entity.id}`,
+    )
+  }
+  // return identity if no filter function
+  return permission.filter || (id => id)
+}
+
+// create a function which creates a new entity and performs authorization checks
+function createCreator(entityName, EntityModel) {
+  return async (input, ctx) => {
+    await can(ctx.user, 'create', entityName)
+    const entity = new EntityModel(input)
+    const outputFilter = await canKnowAbout(ctx.user, entity)
+    await can(ctx.user, 'create', entity)
+
+    return outputFilter(await entity.save())
+  }
+}
+
+// create a function which deletes an entity and performs authorization checks
+function deleteCreator(entityName, EntityModel) {
+  return async (id, ctx) => {
+    await can(ctx.user, 'delete', entityName)
+    const entity = await EntityModel.find(id)
+    const outputFilter = await canKnowAbout(ctx.user, entity)
+    await can(ctx.user, 'delete', entity)
+
+    return outputFilter(await entity.delete())
+  }
+}
+
+// create a function which updates a new entity and performs authorization checks
+function updateCreator(entityName, EntityModel) {
+  return async (id, input, ctx) => {
+    await can(ctx.user, 'update', entityName)
+    const entity = await EntityModel.find(id)
+    const outputFilter = await canKnowAbout(ctx.user, entity)
+    const inputFilter = await can(ctx.user, 'update', entity)
+    await entity.updateProperties(inputFilter(input))
+
+    return outputFilter(await entity.save())
+  }
+}
+
+// create a function which fetches all entities of the
+// given model and performs authorization checks
+function fetchAllCreator(entityName, EntityModel) {
+  return async ctx => {
+    await can(ctx.user, 'read', entityName)
+
+    const entities = await EntityModel.all()
+    // check permissions (in parallel) and swallow exceptions
+    const permissions = await Promise.all(
+      entities.map(entity => can(ctx.user, 'read', entity).catch(() => false)),
+    )
+
+    // apply permissions
+    return entities.reduce((filtered, entity, index) => {
+      const permissionOrFilter = permissions[index]
+
+      if (permissionOrFilter) {
+        filtered.push(permissionOrFilter(entity))
+      }
+      return filtered
+    }, [])
+  }
+}
+
+// create a function which fetches by ID a single entity
+// of the given model and performs authorization checks
+function fetchOneCreator(entityName, EntityModel) {
+  return async (id, ctx) => {
+    await can(ctx.user, 'read', entityName)
+
+    const entity = await EntityModel.find(id)
+    const outputFilter = await canKnowAbout(ctx.user, entity)
+    return outputFilter(entity)
+  }
+}
+
+// create a function which fetches a number of entities by ID
+// and delegates authorization checks
+function fetchSomeCreator(fetchOne) {
+  return (ids, ctx) => Promise.all(ids.map(id => fetchOne(id, ctx)))
+}
+
+// create a connector object with fetchers for all, one and some
+function connectorCreator(entityName, EntityModel) {
+  const create = createCreator(entityName, EntityModel)
+  const del = deleteCreator(entityName, EntityModel)
+  const update = updateCreator(entityName, EntityModel)
+  const fetchAll = fetchAllCreator(entityName, EntityModel)
+  const fetchOne = fetchOneCreator(entityName, EntityModel)
+  const fetchSome = fetchSomeCreator(fetchOne)
+
+  return { create, delete: del, update, fetchAll, fetchOne, fetchSome }
+}
+
+module.exports = { connectorCreator }
diff --git a/packages/server/src/connectors/index.js b/packages/server/src/connectors/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b9c622beb3d6e49179e7ef747d5338601520f8d
--- /dev/null
+++ b/packages/server/src/connectors/index.js
@@ -0,0 +1,12 @@
+const { connectorCreator } = require('./creators')
+const Collection = require('../models/Collection')
+const Fragment = require('../models/Fragment')
+const Team = require('../models/Team')
+const User = require('../models/User')
+
+module.exports = {
+  collection: connectorCreator('collections', Collection),
+  fragment: connectorCreator('fragments', Fragment),
+  team: connectorCreator('teams', Team),
+  user: connectorCreator('users', User),
+}
diff --git a/packages/server/src/graphql/definitions/authentication.js b/packages/server/src/graphql/definitions/authentication.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac427e0547e67e45be4ee12f20ef3fc0002114a1
--- /dev/null
+++ b/packages/server/src/graphql/definitions/authentication.js
@@ -0,0 +1,59 @@
+const logger = require('@pubsweet/logger')
+const User = require('../../models/User')
+const authentication = require('../../authentication')
+
+const resolvers = {
+  Query: {
+    async currentUser(_, vars, ctx) {
+      const user = await User.find(ctx.user)
+      return {
+        user,
+        token: authentication.token.create(user),
+      }
+    },
+  },
+  Mutation: {
+    async loginUser(_, { input }) {
+      let isValid = false
+      let user
+      try {
+        user = await User.findByUsername(input.username)
+        isValid = await user.validPassword(input.password)
+      } catch (err) {
+        logger.debug(err)
+      }
+      if (!isValid) {
+        throw new Error('Wrong username or password.')
+      }
+      return {
+        user,
+        token: authentication.token.create(user),
+      }
+    },
+  },
+}
+
+const typeDefs = `
+  extend type Query {
+    # Get the currently authenticated user based on the JWT in the HTTP headers
+    currentUser: LoginResult
+  }
+
+  extend type Mutation {
+    # Authenticate a user using username and password
+    loginUser(input: LoginUserInput): LoginResult
+  }
+
+  # User details and bearer token
+  type LoginResult {
+    user: User!
+    token: String!
+  }
+
+  input LoginUserInput {
+    username: String!
+    password: String!
+  }
+`
+
+module.exports = { resolvers, typeDefs }
diff --git a/packages/server/src/graphql/definitions/collection.js b/packages/server/src/graphql/definitions/collection.js
new file mode 100644
index 0000000000000000000000000000000000000000..814b7b956b7223b5ce77419e912c6fdc062806ad
--- /dev/null
+++ b/packages/server/src/graphql/definitions/collection.js
@@ -0,0 +1,51 @@
+const resolvers = {
+  Query: {
+    collection(_, { id }, ctx) {
+      return ctx.connectors.collection.fetchOne(id, ctx)
+    },
+    collections(ctx) {
+      return ctx.connectors.collection.fetchAll(ctx)
+    },
+  },
+  Mutation: {
+    deleteCollection(_, { id }, ctx) {
+      return ctx.connectors.collection.delete(id, ctx)
+    },
+    createCollection(_, { input }, ctx) {
+      return ctx.connectors.collection.create(input, ctx)
+    },
+  },
+  Collection: {
+    owners(collection, vars, ctx) {
+      return collection.owners
+        ? ctx.connectors.user.fetchSome(collection.owners, ctx)
+        : []
+    },
+  },
+}
+
+const typeDefs = `
+  extend type Query {
+    collection(id: ID): Collection
+    collections: [Collection]
+  }
+  
+  extend type Mutation {
+    createCollection(input: CollectionInput): Collection
+    deleteCollection(id: ID): Collection 
+  }
+  
+  type Collection {
+    id: ID!
+    type: String!
+    owners: [User!]!
+    fragments: [Fragment!]!
+  }
+  
+  input CollectionInput {
+    owners: [ID!]
+    fragments: [ID!]
+  }
+`
+
+module.exports = { resolvers, typeDefs }
diff --git a/packages/server/src/graphql/definitions/fragment.js b/packages/server/src/graphql/definitions/fragment.js
new file mode 100644
index 0000000000000000000000000000000000000000..0223bdb8bafabd4683cec81e3b5ba3de4a185e9a
--- /dev/null
+++ b/packages/server/src/graphql/definitions/fragment.js
@@ -0,0 +1,52 @@
+const resolvers = {
+  Query: {
+    fragment(_, { id }, ctx) {
+      return ctx.connectors.fragment.fetchOne(id, ctx)
+    },
+    fragments(ctx) {
+      return ctx.connectors.fragment.fetchAll(ctx)
+    },
+  },
+  Mutation: {
+    deleteFragment(_, { id }, ctx) {
+      return ctx.connectors.fragment.delete(id, ctx)
+    },
+    createFragment(_, { input }, ctx) {
+      return ctx.connectors.fragment.create(input, ctx)
+    },
+  },
+  Fragment: {
+    owners(fragment, vars, ctx) {
+      return fragment.owners
+        ? ctx.connectors.user.fetchSome(fragment.owners, ctx)
+        : []
+    },
+  },
+}
+
+const typeDefs = `
+  extend type Query {
+    fragment(id: ID): Fragment
+    fragments: [Fragment]
+  }
+  
+  extend type Mutation {
+    createFragment(input: FragmentInput): Fragment
+    deleteFragment(id: ID): Fragment 
+  }
+  
+  type Fragment {
+    id: ID!
+    type: String!
+    fragmentType: String
+    fragments: [Fragment!]!
+    owners: [User!]!
+  }
+  
+  input FragmentInput {
+    owners: [ID!]
+    fragments: [ID!]
+  }
+`
+
+module.exports = { resolvers, typeDefs }
diff --git a/packages/server/src/graphql/definitions/team.js b/packages/server/src/graphql/definitions/team.js
new file mode 100644
index 0000000000000000000000000000000000000000..18f2e69bf5d4bc060d70ac2da0dd164df456824c
--- /dev/null
+++ b/packages/server/src/graphql/definitions/team.js
@@ -0,0 +1,53 @@
+const resolvers = {
+  Query: {
+    team(_, { id }, ctx) {
+      return ctx.connectors.team.fetchOne(id, ctx)
+    },
+    teams(_, vars, ctx) {
+      return ctx.connectors.team.fetchAll(ctx)
+    },
+  },
+  Mutation: {
+    deleteTeam(_, { id }, ctx) {
+      return ctx.connectors.team.delete(id, ctx)
+    },
+    createTeam(_, { input }, ctx) {
+      return ctx.connectors.team.create(input, ctx)
+    },
+  },
+  Team: {
+    members(team, vars, ctx) {
+      return team.members
+        ? ctx.connectors.user.fetchSome(team.members, ctx)
+        : []
+    },
+  },
+}
+
+const typeDefs = `
+  extend type Query {
+    team(id: ID): Team
+    teams: [Team]
+  }
+  
+  extend type Mutation {
+    createTeam(input: TeamInput): Team
+    deleteTeam(id: ID): Team 
+  }
+  
+  type Team {
+    id: ID!
+    type: String!
+    teamType: String!
+    name: String!
+    object: ID!
+    members: [User!]!
+  }
+  
+  input TeamInput {
+    owners: [ID!]
+    fragments: [ID!]
+  }
+`
+
+module.exports = { resolvers, typeDefs }
diff --git a/packages/server/src/graphql/definitions/user.js b/packages/server/src/graphql/definitions/user.js
new file mode 100644
index 0000000000000000000000000000000000000000..29ecbcd824730b782761a04ff9b1c2b7d42fda45
--- /dev/null
+++ b/packages/server/src/graphql/definitions/user.js
@@ -0,0 +1,69 @@
+const resolvers = {
+  Query: {
+    user(_, { id }, ctx) {
+      return ctx.connectors.user.fetchOne(id, ctx)
+    },
+    users(_, vars, ctx) {
+      return ctx.connectors.user.fetchAll(ctx)
+    },
+  },
+  Mutation: {
+    createUser(_, { input }, ctx) {
+      return ctx.connectors.user.create(input, ctx)
+    },
+    deleteUser(_, { id }, ctx) {
+      return ctx.connectors.user.delete(id, ctx)
+    },
+    updateUser(_, { id, input }, ctx) {
+      return ctx.connectors.user.update(id, input, ctx)
+    },
+  },
+  User: {
+    collections(user, vars, ctx) {
+      return user.collections
+        ? ctx.connectors.collection.fetchSome(user.collections, ctx)
+        : []
+    },
+    teams(user, vars, ctx) {
+      return user.teams ? ctx.connectors.team.fetchSome(user.teams, ctx) : []
+    },
+    fragments(user, vars, ctx) {
+      return user.fragments
+        ? ctx.connectors.fragment.fetchSome(user.fragments, ctx)
+        : []
+    },
+  },
+}
+
+const typeDefs = `
+  extend type Query {
+    user(id: ID): User
+    users: [User]
+  }
+  
+  extend type Mutation {
+    createUser(input: UserInput): User
+    deleteUser(id: ID): User 
+    updateUser(id: ID, input: UserInput): User 
+  }
+  
+  type User {
+    id: ID!
+    type: String
+    username: String!
+    email: String!
+    admin: Boolean
+    teams: [Team!]!
+    fragments: [Fragment!]!
+    collections: [Collection!]!
+  }
+  
+  input UserInput {
+    username: String!
+    email: String!
+    password: String
+    rev: String
+  }
+`
+
+module.exports = { resolvers, typeDefs }
diff --git a/packages/server/src/graphql/routes.js b/packages/server/src/graphql/routes.js
new file mode 100644
index 0000000000000000000000000000000000000000..680380e1c13a7d9056883260cb28065b66e0884c
--- /dev/null
+++ b/packages/server/src/graphql/routes.js
@@ -0,0 +1,33 @@
+const express = require('express')
+const passport = require('passport')
+const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
+const config = require('config')
+
+const graphqlSchema = require('./schema')
+const connectors = require('../connectors')
+
+const authBearerAndPublic = passport.authenticate(['bearer', 'anonymous'], {
+  session: false,
+})
+const router = new express.Router()
+
+router.use(
+  '/graphql',
+  authBearerAndPublic,
+  graphqlExpress(req => ({
+    schema: graphqlSchema,
+    context: { user: req.user, connectors },
+  })),
+)
+if (
+  config.has('pubsweet-server.graphiql') &&
+  config.get('pubsweet-server.graphiql')
+) {
+  router.get(
+    '/graphiql',
+    authBearerAndPublic,
+    graphiqlExpress({ endpointURL: '/graphql' }),
+  )
+}
+
+module.exports = router
diff --git a/packages/server/src/graphql/schema.js b/packages/server/src/graphql/schema.js
new file mode 100644
index 0000000000000000000000000000000000000000..7f0803113a0127114dd2f90e534b0d137086869b
--- /dev/null
+++ b/packages/server/src/graphql/schema.js
@@ -0,0 +1,51 @@
+const config = require('config')
+const requireRelative = require('require-relative')
+const { merge } = require('lodash')
+const { makeExecutableSchema } = require('graphql-tools')
+
+const collection = require('./definitions/collection')
+const fragment = require('./definitions/fragment')
+const team = require('./definitions/team')
+const user = require('./definitions/user')
+const authentication = require('./definitions/authentication')
+
+// load base types and resolvers
+const typeDefs = [
+  `type Query, type Mutation`,
+  collection.typeDefs,
+  fragment.typeDefs,
+  team.typeDefs,
+  user.typeDefs,
+  authentication.typeDefs,
+]
+const resolvers = merge(
+  {},
+  collection.resolvers,
+  fragment.resolvers,
+  team.resolvers,
+  user.resolvers,
+  authentication.resolvers,
+)
+
+// merge in component types and resolvers
+if (config.has('pubsweet.components')) {
+  config.get('pubsweet.components').forEach(name => {
+    const component = requireRelative(name)
+    if (component.typeDefs) {
+      typeDefs.push(component.typeDefs)
+    }
+    if (component.resolvers) {
+      merge(resolvers, component.resolvers)
+    }
+  })
+}
+
+// merge in app-specific types and resolvers from config
+if (config.has('pubsweet-server.typeDefs')) {
+  typeDefs.push(config.get('pubsweet-server.typeDefs'))
+}
+if (config.has('pubsweet-server.resolvers')) {
+  merge(resolvers, config.get('pubsweet-server.resolvers'))
+}
+
+module.exports = makeExecutableSchema({ typeDefs, resolvers })
diff --git a/packages/server/src/index.js b/packages/server/src/index.js
index 20d4a476594fb6af9bf4dc4c886ca52005d458fa..7110e0bf01d6d2777a1f6c2d213ac8b8432eb5d6 100644
--- a/packages/server/src/index.js
+++ b/packages/server/src/index.js
@@ -10,6 +10,7 @@ const helmet = require('helmet')
 const cookieParser = require('cookie-parser')
 const bodyParser = require('body-parser')
 const passport = require('passport')
+const graphqlApi = require('./graphql/routes')
 const index = require('./routes/index')
 const api = require('./routes/api')
 const authsome = require('./helpers/authsome')
@@ -47,9 +48,15 @@ const configureApp = app => {
 
   registerComponents(app)
 
-  // Main API
+  // REST API
   app.use('/api', api)
 
+  // GraphQL API
+  // temporary environment check while this stuff is in beta
+  if (['development', 'test'].includes(config.util.getEnv('NODE_ENV'))) {
+    app.use(graphqlApi)
+  }
+
   // SSE update stream
   if (_.get('pubsweet-server.sse', config)) {
     app.get(
diff --git a/packages/server/test/graphql_test.js b/packages/server/test/graphql_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..a15039018202350ff91c4bca87f74a1ff575e6a0
--- /dev/null
+++ b/packages/server/test/graphql_test.js
@@ -0,0 +1,226 @@
+const { omit } = require('lodash')
+const authsome = require('../src/helpers/authsome')
+const User = require('../src/models/User')
+const Team = require('../src/models/Team')
+const cleanDB = require('./helpers/db_cleaner')
+const fixtures = require('./fixtures/fixtures')
+const api = require('./helpers/api')
+const authentication = require('../src/authentication')
+
+describe('GraphQL endpoint', () => {
+  let token
+  let user
+  beforeEach(async () => {
+    await cleanDB()
+    user = await new User(fixtures.adminUser).save()
+    token = authentication.token.create(user)
+  })
+
+  describe('queries', () => {
+    it('can resolve all users', async () => {
+      const { body } = await api.graphql.query(
+        `{ users { username, admin } }`,
+        {},
+        token,
+      )
+
+      expect(body).toEqual({
+        data: { users: [{ username: 'admin', admin: true }] },
+      })
+    })
+
+    it('can resolve user by ID', async () => {
+      const { body } = await api.graphql.query(
+        `query($id: ID) {
+          user(id: $id) { 
+            username
+            admin 
+          }
+        }`,
+        { id: user.id },
+        token,
+      )
+
+      expect(body).toEqual({
+        data: { user: { username: 'admin', admin: true } },
+      })
+    })
+
+    it('can resolve nested query', async () => {
+      await new Team({ ...fixtures.contributorTeam, members: [user.id] }).save()
+      const { body } = await api.graphql.query(
+        `{ users { username, teams { name } } }`,
+        {},
+        token,
+      )
+
+      expect(body).toEqual({
+        data: {
+          users: [{ username: 'admin', teams: [{ name: 'My contributors' }] }],
+        },
+      })
+    })
+  })
+
+  describe('mutations', () => {
+    it('can create a user', async () => {
+      const { body } = await api.graphql.query(
+        `mutation($input: UserInput) { 
+          createUser(input: $input) { username } 
+        }`,
+        {
+          input: {
+            username: 'floobs',
+            email: 'nobody@example.com',
+            password: 'password',
+          },
+        },
+        token,
+      )
+
+      expect(body).toEqual({
+        data: {
+          createUser: { username: 'floobs' },
+        },
+      })
+    })
+
+    it('can update a user', async () => {
+      const { body } = await api.graphql.query(
+        `mutation($id: ID, $input: UserInput) { 
+          updateUser(id: $id, input: $input) { username, email } 
+        }`,
+        {
+          id: user.id,
+          input: {
+            username: 'floobs',
+            email: 'nobody@example.com',
+            rev: user.rev,
+          },
+        },
+        token,
+      )
+
+      expect(body).toEqual({
+        data: {
+          updateUser: { username: 'floobs', email: 'nobody@example.com' },
+        },
+      })
+    })
+
+    it('can delete a user', async () => {
+      const { body } = await api.graphql.query(
+        `mutation($id: ID) { 
+          deleteUser(id: $id) { username } 
+        }`,
+        { id: user.id },
+        token,
+      )
+
+      expect(body).toEqual({
+        data: { deleteUser: { username: 'admin' } },
+      })
+    })
+  })
+
+  describe('auth', () => {
+    it('can log in', async () => {
+      const { body } = await api.graphql.query(
+        `mutation($input: LoginUserInput) {
+          loginUser(input: $input) {
+            user { username }
+            token
+          }
+        }`,
+        { input: { username: 'admin', password: 'admin' } },
+      )
+
+      expect(body).toMatchObject({
+        data: {
+          loginUser: { token: expect.any(String), user: { username: 'admin' } },
+        },
+      })
+    })
+
+    it('blocks invalid login', async () => {
+      const { body } = await api.graphql.query(
+        `mutation($input: LoginUserInput) {
+          loginUser(input: $input) {
+            token
+          }
+        }`,
+        { input: { username: 'admin', password: 'not correct' } },
+      )
+
+      expect(body).toMatchObject({
+        data: { loginUser: null },
+        errors: [{ message: 'Wrong username or password.' }],
+      })
+    })
+
+    it('fetches current user from token', async () => {
+      const { body } = await api.graphql.query(
+        `{ currentUser { user { username, email } token} }`,
+        {},
+        token,
+      )
+
+      expect(body).toMatchObject({
+        data: {
+          currentUser: {
+            user: { username: 'admin', email: 'admin@admins.example.org' },
+            token: expect.any(String),
+          },
+        },
+      })
+    })
+
+    it('errors when unauthenticated', async () => {
+      const { body } = await api.graphql.query(`{ users { username } }`)
+
+      expect(body).toMatchObject({
+        data: { users: null },
+        errors: [{ message: 'Operation not permitted: read users' }],
+      })
+    })
+
+    it('filters the returned data', async () => {
+      jest
+        .spyOn(authsome, 'can')
+        .mockReturnValue({ filter: user => omit(user, 'admin') })
+
+      const { body } = await api.graphql.query(
+        `{ users { username, admin } }`,
+        {},
+        token,
+      )
+
+      expect(body).toEqual({
+        data: { users: [{ username: 'admin', admin: null }] },
+      })
+    })
+
+    it('returns not found if not authorized', async () => {
+      jest
+        .spyOn(authsome, 'can')
+        .mockReturnValueOnce(true)
+        .mockReturnValueOnce(false)
+
+      const { body } = await api.graphql.query(
+        `query($id: ID) {
+          user(id: $id) { 
+            username
+            admin 
+          }
+        }`,
+        { id: user.id },
+        token,
+      )
+
+      expect(body).toMatchObject({
+        data: { user: null },
+        errors: [{ message: `Object not found: user with id ${user.id}` }],
+      })
+    })
+  })
+})
diff --git a/packages/server/test/helpers/api.js b/packages/server/test/helpers/api.js
index 44d6cc449384b68e289a40a9947fc2b6eb1a359d..7fe3fb3912bc4e67dcb8cb5e1a42f98308afac78 100644
--- a/packages/server/test/helpers/api.js
+++ b/packages/server/test/helpers/api.js
@@ -285,11 +285,22 @@ const upload = {
   },
 }
 
+const graphql = {
+  query: (query, variables, token) => {
+    const req = request(api)
+      .post('/graphql')
+      .send({ query, variables })
+    if (token) req.set('Authorization', `Bearer ${token}`)
+    return req
+  },
+}
+
 module.exports = {
   fragments,
   users,
   collections,
   teams,
   upload,
+  graphql,
   api,
 }
diff --git a/packages/server/test/mocks/mock_component.js b/packages/server/test/mocks/mock_component.js
index 73803ecbdd56675d122e91a9969014be9cf0caa1..2717024918f4cbb3e8eda4560ed55f7d0072f1b2 100644
--- a/packages/server/test/mocks/mock_component.js
+++ b/packages/server/test/mocks/mock_component.js
@@ -6,6 +6,8 @@ const mockComponent = {
       res.status(STATUS.OK).json({ ok: '!' }),
     )
   },
+  typeDefs: `extend type Query { test: String }`,
+  resolvers: { Query: { test: () => 'OK' } },
 }
 
 module.exports = mockComponent
diff --git a/packages/server/test/register_components_test.js b/packages/server/test/register_components_test.js
index 3af0161ac694573b949d8455664059af67ae84e7..aed8e1be86fc2215a50ce6fa0a8d1a7dd4cbbbd8 100644
--- a/packages/server/test/register_components_test.js
+++ b/packages/server/test/register_components_test.js
@@ -13,4 +13,9 @@ describe('App startup', async () => {
     const res = await request(api.api).get('/mock-component')
     expect(res.status).toBe(STATUS.OK)
   })
+
+  it('loads graphql types and resolvers', async () => {
+    const res = await api.graphql.query('{ test }')
+    expect(res.body).toEqual({ data: { test: 'OK' } })
+  })
 })
diff --git a/yarn.lock b/yarn.lock
index f6ad2aaca9d381467cf71736cef8fbe84e4c2aa7..a0a33b1e2d9e9e2c2a29e9b0c6ec960c5e487f09 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -221,6 +221,10 @@
   version "8.0.58"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.58.tgz#5b3881c0be3a646874803fee3197ea7f1ed6df90"
 
+"@types/zen-observable@0.5.3":
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.3.tgz#91b728599544efbb7386d8b6633693a3c2e7ade5"
+
 JSONStream@^1.0.4:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
@@ -415,6 +419,49 @@ anymatch@^1.3.0:
     micromatch "^2.1.5"
     normalize-path "^2.0.0"
 
+apollo-cache-control@^0.0.x:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.0.9.tgz#77100f456fb19526d33b7f595c8ab1a2980dcbb4"
+  dependencies:
+    graphql-extensions "^0.0.x"
+
+apollo-link@^1.0.0:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.0.7.tgz#42cd38a7378332fc3e41a214ff6a6e5e703a556f"
+  dependencies:
+    "@types/zen-observable" "0.5.3"
+    apollo-utilities "^1.0.0"
+    zen-observable "^0.6.0"
+
+apollo-server-core@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.2.tgz#f36855a3ebdc2d77b8b9c454380bf1d706105ffc"
+  dependencies:
+    apollo-cache-control "^0.0.x"
+    apollo-tracing "^0.1.0"
+    graphql-extensions "^0.0.x"
+
+apollo-server-express@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.3.2.tgz#0ff8201c0bf362804a151e1399767dae6ab7e309"
+  dependencies:
+    apollo-server-core "^1.3.2"
+    apollo-server-module-graphiql "^1.3.0"
+
+apollo-server-module-graphiql@^1.3.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.3.2.tgz#0a9e4c48dece3af904fee333f95f7b9817335ca7"
+
+apollo-tracing@^0.1.0:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.3.tgz#6820c066bf20f9d9a4eddfc023f7c83ee2601f0b"
+  dependencies:
+    graphql-extensions "^0.0.x"
+
+apollo-utilities@^1.0.0, apollo-utilities@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.4.tgz#560009ea5541b9fdc4ee07ebb1714ee319a76c15"
+
 app-root-path@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
@@ -2635,7 +2682,7 @@ core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
-core-js@^2.4.0, core-js@^2.5.0:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3:
   version "2.5.3"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
 
@@ -3085,6 +3132,10 @@ depd@1.1.1, depd@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
 
+deprecated-decorator@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37"
+
 des.js@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -4872,6 +4923,36 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3,
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
+graphql-extensions@^0.0.x:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.7.tgz#807e7c3493da45e8f8fd02c0da771a9b3f1f2d1a"
+  dependencies:
+    core-js "^2.5.3"
+    source-map-support "^0.5.1"
+
+graphql-subscriptions@^0.5.6:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.6.tgz#0d8e960fbaaf9ecbe7900366e86da2fc143fc5b2"
+  dependencies:
+    es6-promise "^4.1.1"
+    iterall "^1.1.3"
+
+graphql-tools@^2.18.0:
+  version "2.18.0"
+  resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.18.0.tgz#8e2d6436f9adba1d579c1a1710ae95e7f5e7248b"
+  dependencies:
+    apollo-link "^1.0.0"
+    apollo-utilities "^1.0.1"
+    deprecated-decorator "^0.1.6"
+    graphql-subscriptions "^0.5.6"
+    uuid "^3.1.0"
+
+graphql@^0.12.3:
+  version "0.12.3"
+  resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.12.3.tgz#11668458bbe28261c0dcb6e265f515ba79f6ce07"
+  dependencies:
+    iterall "1.1.3"
+
 growly@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -5982,6 +6063,10 @@ istanbul-reports@^1.1.3:
   dependencies:
     handlebars "^4.0.3"
 
+iterall@1.1.3, iterall@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9"
+
 javascript-stringify@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
@@ -10553,6 +10638,12 @@ source-map-support@^0.5.0:
   dependencies:
     source-map "^0.6.0"
 
+source-map-support@^0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.2.tgz#1a6297fd5b2e762b39688c7fc91233b60984f0a5"
+  dependencies:
+    source-map "^0.6.0"
+
 source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -12326,6 +12417,10 @@ yargs@~3.10.0:
     decamelize "^1.0.0"
     window-size "0.1.0"
 
+zen-observable@^0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.6.1.tgz#01dbed3bc8d02cbe9ee1112c83e04c807f647244"
+
 zip-stream@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"