diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..321c3475d6f10c524b8cf6e446201d97b84fb6b7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,49 @@
+# docker
+Dockerfile*
+docker-compose*
+
+# git
+.git
+.gitignore
+
+# npm & yarn
+node_modules
+npm-debug.log
+package-lock.json
+yarn-error.log
+
+# misc
+.devcontainer
+.DS_Store
+.vscode
+app.pid
+
+# environment files
+.env*
+*.env
+
+# app
+_build/
+config/local*
+data/
+coverage/
+cypress/
+logs/
+stories/
+tmp/
+uploads/
+templates
+
+# Include README.md and LICENSE.md, exlcude the rest
+*.md
+!README.md
+!LICENSE.md
+
+# development config files
+# .eslintrc.js
+.commitlintrc.js
+.cz-config.js
+.gitlab-ci.yml
+.linstagedrc.js
+# .prettierrc.js
+.stylelintrc.js
\ No newline at end of file
diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js
index e121ec6ff70d83cdb7c7e1f21cdd8eaf7fbdfdd5..b73750904cca89cb88a164520ad3a9edbda5536c 100644
--- a/config/custom-environment-variables.js
+++ b/config/custom-environment-variables.js
@@ -22,6 +22,7 @@ module.exports = {
     minioConsolePort: 'MINIO_CONSOLE_PORT',
     maximumWidthForSmallImages: 'MAXIMUM_WIDTH_FOR_SMALL_IMAGES',
     maximumWidthForMediumImages: 'MAXIMUM_WIDTH_FOR_MEDIUM_IMAGES',
+    s3SeparateDeleteOperations: 'S3_SEPARATE_DELETE_OPERATIONS',
   },
   chatGPT: {
     key: 'CHAT_GPT_KEY',
diff --git a/config/default.js b/config/default.js
index 26e09261e7f8f44c90481eb6362f57f11ecaa12a..247b323512d9293759e4487cf9968049b0c3edcf 100644
--- a/config/default.js
+++ b/config/default.js
@@ -60,5 +60,6 @@ module.exports = {
     host: 'localhost',
     port: '9000',
     minioConsolePort: '9001',
+    s3SeparateDeleteOperations: false,
   },
 }
diff --git a/src/__tests__/job.test.js b/src/__tests__/job.test.js
index 3c95ba162c7c9470c412b07f4c5e03cc0d26023a..0d6aead5726a6cf814156b777e4a20f4d716836d 100644
--- a/src/__tests__/job.test.js
+++ b/src/__tests__/job.test.js
@@ -1,7 +1,7 @@
 const { boss } = require('pubsweet-server/src/jobs')
 const { subscribeJobsToQueue } = require('../jobs')
 const { jobs } = require('../services')
-const { renewAuthTokens } = require('../utils/tokens')
+// const { renewAuthTokens } = require('../utils/tokens')
 
 const freezeTime = 1701856542000
 const daySeconds = 24 * 3600
@@ -56,6 +56,16 @@ jest.mock('../utils/tokens', () => {
   }
 })
 
+jest.mock('../models/user/user.controller', () => {
+  const originalModule = jest.requireActual('../models/user/user.controller')
+  return {
+    __esModule: true,
+    ...originalModule,
+    getUser: jest.fn(async userId => ({
+      id: userId,
+    })),
+  }
+})
 // Mock the date and time
 Date.now = jest.fn(() => freezeTime)
 
@@ -77,42 +87,10 @@ describe('jobs service', () => {
 
   it('registers jobs', async () => {
     expect(Object.keys(boss.subscriptions)).toEqual([
-      jobs.RENEW_AUTH_TOKENS_JOB,
+      jobs.REFRESH_TOKEN_EXPIRED,
     ])
     expect(
-      typeof boss.subscriptions[jobs.RENEW_AUTH_TOKENS_JOB].callback,
+      typeof boss.subscriptions[jobs.REFRESH_TOKEN_EXPIRED].callback,
     ).toEqual('function')
   })
-
-  it('reschedules auth token renewal after successfully renewing the refresh token', async () => {
-    boss.log = []
-
-    // Run the job callback directly and then verify its behaviour
-    const renewCallback =
-      boss.subscriptions[jobs.RENEW_AUTH_TOKENS_JOB].callback
-
-    const job = dummyJob(
-      { userId: 'fakeUserId', providerLabel: 'fakeProviderLabel' },
-      {},
-    )
-
-    await renewCallback(job)
-
-    // renewAuthTokens should have been called
-    expect(renewAuthTokens.mock.calls.length).toEqual(1)
-    expect(renewAuthTokens.mock.calls[0]).toEqual([
-      'fakeUserId',
-      'fakeProviderLabel',
-    ])
-
-    // Job should succeed and be marked done
-    expect(job.isDone).toBe(true)
-
-    // Job should schedule a future job
-    expect(boss.log).toEqual([`publish ${jobs.RENEW_AUTH_TOKENS_JOB}`])
-    expect(boss.lastJob.data).toEqual(job.data)
-    expect(Object.keys(boss.lastJob.options).length).toEqual(1)
-    // Refresh token expires in 7 days and must be renewed in 6
-    expect(boss.lastJob.options.startAfter).toEqual(daySeconds * 6)
-  })
 })
diff --git a/src/jobs.js b/src/jobs.js
index c073acc4bc831b5e2ea90dea7fad7662bd652f7a..4eb43b22cad1b03b32c0e7bde67ea7044a6d02b7 100644
--- a/src/jobs.js
+++ b/src/jobs.js
@@ -104,6 +104,7 @@ const defaultJobs = [
         pubsub.publish(USER_UPDATED, {
           userUpdated: updatedUser,
         })
+        job.done()
       } catch (e) {
         logger.error(`Job ${jobs.REFRESH_TOKEN_EXPIRED}: defer error:`, e)
         throw e
diff --git a/src/models/__tests__/identity.controller.test.js b/src/models/__tests__/identity.controller.test.js
index bd78a43212755e8a706d52127c3761a84282887a..db440358ea1b94a81405f8d9e07296f987ab9e28 100644
--- a/src/models/__tests__/identity.controller.test.js
+++ b/src/models/__tests__/identity.controller.test.js
@@ -151,8 +151,10 @@ describe('Identity Controller', () => {
     // Expect renewal job to have been "scheduled"
     const lastCallIndex = jobs.defer.mock.calls.length - 1
     const [name, renewAfter, data] = jobs.defer.mock.calls[lastCallIndex]
-    expect(name).toEqual('renew-auth-tokens')
-    expect(renewAfter).toEqual({ seconds: 273600 }) // 360000 - 86400
+    expect(name).toEqual('refresh-token-expired')
+    expect({ seconds: Math.round(renewAfter.seconds) }).toEqual({
+      seconds: 360000,
+    }) // 360000 - 86400
     expect(data).toEqual({ providerLabel: 'test', userId: user.id })
   })
 })
diff --git a/src/services/fileStorage.js b/src/services/fileStorage.js
index b6e3bc5f5758478bc3ff7e9cf8398d0f203f19e2..3ec7f01b3e3699d28fd29e3ee39c86903dc3704a 100644
--- a/src/services/fileStorage.js
+++ b/src/services/fileStorage.js
@@ -479,12 +479,37 @@ const deleteFiles = objectKeys => {
     )
   }
 
-  const { bucket } = config.get('fileStorage')
+  const { bucket, s3SeparateDeleteOperations } = config.get('fileStorage')
 
   if (objectKeys.length === 0) {
     throw new Error('the provided array of keys if empty')
   }
 
+  const separateDeleteOperations = !emptyUndefinedOrNull(
+    s3SeparateDeleteOperations,
+  )
+    ? JSON.parse(s3SeparateDeleteOperations)
+    : false
+
+  if (separateDeleteOperations) {
+    return Promise.all(
+      objectKeys.map(
+        async objectKey =>
+          new Promise((resolve, reject) => {
+            const params = { Bucket: bucket, Key: objectKey }
+
+            s3.deleteObject(params, (err, data) => {
+              if (err) {
+                reject(err)
+              }
+
+              resolve(data)
+            })
+          }),
+      ),
+    )
+  }
+
   const params = {
     Bucket: bucket,
     Delete: {
diff --git a/src/services/jobs/jobs.identifiers.js b/src/services/jobs/jobs.identifiers.js
index 27f5a2acf88341e749cc97959a119b0a00d34625..ab7fdd68485d2e5a25125836ff995c7953b05dd7 100644
--- a/src/services/jobs/jobs.identifiers.js
+++ b/src/services/jobs/jobs.identifiers.js
@@ -1,4 +1,4 @@
 module.exports = {
-  RENEW_AUTH_TOKENS_JOB: 'renew-auth-tokens',
+  // RENEW_AUTH_TOKENS_JOB: 'renew-auth-tokens',
   REFRESH_TOKEN_EXPIRED: 'refresh-token-expired',
 }