diff --git a/config/components.js b/config/components.js
index 6ac80082e3aa0195a1630c0fc98646e6b6f211d7..5b76703715ec6fc54ec6b74972399aaf627dd818 100644
--- a/config/components.js
+++ b/config/components.js
@@ -9,4 +9,5 @@ module.exports = [
   './src/models/__tests__/helpers/fake',
   './src/models/serviceCredential',
   './src/services/chatGPT',
+  './src/models/activityLog',
 ]
diff --git a/dev/config/components.js b/dev/config/components.js
index bc4acd40d2a74a9b72de525fd5e6f695e7a1f828..9420b5dc0542fee65757eb63c1cbe8b06664565c 100644
--- a/dev/config/components.js
+++ b/dev/config/components.js
@@ -3,4 +3,5 @@ module.exports = [
   './src/models/teamMember',
   './src/models/user',
   './src/models/identity',
+  './src/models/activityLog',
 ]
diff --git a/src/index.js b/src/index.js
index d40fc5eb9de1cab8b866bd5501aead1580f94305..e1c5124bf93f38fc5e59a8518e90fa665601ba94 100644
--- a/src/index.js
+++ b/src/index.js
@@ -27,6 +27,8 @@ const {
 
 const WaxToDocxConverter = require('./services/docx/docx.service')
 
+const activityLog = require('./services/activityLog')
+
 // Do not expose connectToFileStorage
 const fileStorage = {
   healthCheck,
@@ -62,7 +64,7 @@ module.exports = {
   deleteFiles,
   // serviceHandshake,
   sendEmail,
-
+  activityLog,
   BaseModel,
   File,
   logger,
diff --git a/src/models/activityLog/activityLog.model.js b/src/models/activityLog/activityLog.model.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa3f417f2e4fd3b3287c6a4a5d8af07466727e74
--- /dev/null
+++ b/src/models/activityLog/activityLog.model.js
@@ -0,0 +1,71 @@
+const BaseModel = require('../base.model')
+
+const {
+  id,
+  stringNotEmpty,
+  stringNullable,
+  object,
+  objectNullable,
+} = require('../_helpers/types')
+
+const affectedObject = {
+  type: 'object',
+  additionalProperties: false,
+  required: ['id', 'objectType'],
+  properties: {
+    id,
+    objectType: stringNotEmpty,
+  },
+}
+
+const affectedObjects = {
+  type: 'array',
+  default: [],
+  additionalProperties: false,
+  items: affectedObject,
+}
+
+class ActivityLog extends BaseModel {
+  constructor(properties) {
+    super(properties)
+    this.type = 'activityLog'
+  }
+
+  static get tableName() {
+    return 'activityLogs'
+  }
+
+  static get schema() {
+    return {
+      type: 'object',
+      required: ['actorId', 'actionType'],
+      properties: {
+        actorId: id,
+        actionType: stringNotEmpty,
+        message: stringNullable,
+        valueBefore: objectNullable,
+        valueAfter: objectNullable,
+        affectedObjects,
+        additionalData: object,
+      },
+    }
+  }
+
+  static get relationMappings() {
+    /* eslint-disable-next-line global-require */
+    const User = require('../user/user.model')
+
+    return {
+      user: {
+        relation: BaseModel.BelongsToOneRelation,
+        modelClass: User,
+        join: {
+          from: 'activityLogs.actorId',
+          to: 'users.id',
+        },
+      },
+    }
+  }
+}
+
+module.exports = ActivityLog
diff --git a/src/models/activityLog/constants.js b/src/models/activityLog/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..9fedce182240fbf078439076092f2f5077b6240b
--- /dev/null
+++ b/src/models/activityLog/constants.js
@@ -0,0 +1,11 @@
+module.exports = {
+  labels: {
+    ACTIVITY_LOG_SERVICE: '[ACTIVITY_LOG_SERVICE] -',
+  },
+  actionTypes: {
+    CREATE: 'CREATE',
+    UPDATE: 'UPDATE',
+    PATCH: 'PATCH',
+    DELETE: 'DELETE',
+  },
+}
diff --git a/src/models/activityLog/index.js b/src/models/activityLog/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..21d8d1155c40307fcace74b7420aedd506e21f3a
--- /dev/null
+++ b/src/models/activityLog/index.js
@@ -0,0 +1,6 @@
+const model = require('./activityLog.model')
+
+module.exports = {
+  model,
+  modelName: 'ActivityLog',
+}
diff --git a/src/models/activityLog/migrations/1696505906-activityLog-init.js b/src/models/activityLog/migrations/1696505906-activityLog-init.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd53ee52aa027503eda3ecaf9841ab86b7584475
--- /dev/null
+++ b/src/models/activityLog/migrations/1696505906-activityLog-init.js
@@ -0,0 +1,30 @@
+const logger = require('@pubsweet/logger')
+
+exports.up = knex => {
+  try {
+    return knex.schema.createTable('activity_logs', table => {
+      table.uuid('id').primary()
+      table
+        .timestamp('created', { useTz: true })
+        .notNullable()
+        .defaultTo(knex.fn.now())
+      table.timestamp('updated', { useTz: true })
+
+      table.uuid('actor_id').references('users.id').notNullable()
+      table.text('action_type').notNullable()
+
+      table.jsonb('value_before')
+      table.jsonb('value_after')
+      table.jsonb('affected_objects').defaultTo([])
+      table.text('message')
+      table.jsonb('additional_data')
+
+      table.text('type').notNullable()
+    })
+  } catch (e) {
+    logger.error('Acitivity log: Initial: Migration failed!')
+    throw new Error(e)
+  }
+}
+
+exports.down = knex => knex.schema.dropTable('activity_logs')
diff --git a/src/models/index.js b/src/models/index.js
index a227b14a0de8e1ace7cb14907f30981a8b3aeaab..def3682158d3bbd4ab11d76daf851cff9a8cb540 100644
--- a/src/models/index.js
+++ b/src/models/index.js
@@ -10,6 +10,7 @@ const User = require('./user/user.model')
 const Identity = require('./identity/identity.model')
 
 const File = require('./file/file.model')
+const ActivityLog = require('./activityLog/activityLog.model')
 
 const useTransaction = require('./useTransaction')
 const ServiceCredential = require('./serviceCredential/serviceCredential.model')
@@ -27,6 +28,7 @@ module.exports = {
   Identity,
 
   File,
+  ActivityLog,
 
   useTransaction,
   ServiceCredential,
diff --git a/src/services/__tests__/activityLog.service.test.js b/src/services/__tests__/activityLog.service.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..032f373e2e78a8ecaa0a761d1fb09daed1b5788c
--- /dev/null
+++ b/src/services/__tests__/activityLog.service.test.js
@@ -0,0 +1,33 @@
+const activityLog = require('../activityLog')
+
+const Fake = require('../../models/__tests__/helpers/fake/fake.model')
+const { createUser } = require('../../models/__tests__/helpers/users')
+const clearDb = require('../../models/__tests__/_clearDb')
+const ActivityLog = require('../../models/activityLog/activityLog.model')
+const { actionTypes } = require('../../models/activityLog/constants')
+
+describe('Activity Log Service', () => {
+  beforeEach(() => clearDb())
+
+  afterAll(() => {
+    const knex = Fake.knex()
+    knex.destroy()
+  })
+
+  it('creates a log entry', async () => {
+    const actor = await createUser()
+    const dummyUser = await createUser()
+
+    const log = await activityLog({
+      actorId: actor.id,
+      actionType: actionTypes.CREATE,
+      message: 'create a new user',
+      valueAfter: dummyUser,
+      affectedObjects: [{ id: dummyUser.id, objectType: 'user' }],
+    })
+
+    const { result: activities } = await ActivityLog.find({})
+    expect(log).toBeDefined()
+    expect(activities).toHaveLength(1)
+  })
+})
diff --git a/src/services/activityLog.js b/src/services/activityLog.js
new file mode 100644
index 0000000000000000000000000000000000000000..24e0faaeb1013ec57381fb7f13f347954ad20829
--- /dev/null
+++ b/src/services/activityLog.js
@@ -0,0 +1,29 @@
+const logger = require('@pubsweet/logger')
+
+const useTransaction = require('../models/useTransaction')
+const ActivityLog = require('../models/activityLog/activityLog.model')
+
+const {
+  labels: { ACTIVITY_LOG_SERVICE },
+} = require('../models/activityLog/constants')
+
+const activityLog = async (data, options = {}) => {
+  try {
+    const { trx } = options
+
+    return useTransaction(
+      async tr => {
+        return ActivityLog.insert(data, { trx: tr })
+      },
+      {
+        trx,
+        passedTrxOnly: true,
+      },
+    )
+  } catch (e) {
+    logger.error(`${ACTIVITY_LOG_SERVICE} activityLog: ${e.message}`)
+    throw new Error(e)
+  }
+}
+
+module.exports = activityLog