diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index 9501212b6c92e77f1d8d40e012dd44af79a6503c..b388c8c358af83c2462410068a445b1c2502a52c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -32,6 +32,8 @@ import org.briarproject.bramble.util.OsUtils.isLinux
 import org.briarproject.bramble.util.OsUtils.isMac
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
+import org.briarproject.briar.desktop.viewmodel.BriarExecutorsImpl
 import org.briarproject.briar.desktop.viewmodel.UiExecutor
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
 import java.io.File
@@ -83,6 +85,10 @@ internal class DesktopModule(
     @UiExecutor
     fun provideUiExecutor(): Executor = Dispatchers.Swing.asExecutor()
 
+    @Provides
+    @Singleton
+    fun provideBriarExecutors(briarExecutors: BriarExecutorsImpl): BriarExecutors = briarExecutors
+
     @Provides
     @TorDirectory
     internal fun provideTorDirectory(): File {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
index 3e9063e88c16213b63a53f0956477e697788470d..7bb48edd6f12fe714943ea113efe19a16970de91 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
@@ -7,7 +7,6 @@ import org.briarproject.bramble.api.connection.ConnectionRegistry
 import org.briarproject.bramble.api.contact.ContactId
 import org.briarproject.bramble.api.contact.ContactManager
 import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent
-import org.briarproject.bramble.api.db.DatabaseExecutor
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
@@ -15,8 +14,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.conversation.event.ConversationMessageTrackedEvent
 import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent
-import org.briarproject.briar.desktop.viewmodel.UiExecutor
-import java.util.concurrent.Executor
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
 import javax.inject.Inject
 
 class ContactListViewModel
@@ -25,13 +23,12 @@ constructor(
     contactManager: ContactManager,
     conversationManager: ConversationManager,
     connectionRegistry: ConnectionRegistry,
-    @UiExecutor uiExecutor: Executor,
-    @DatabaseExecutor dbExecutor: Executor,
+    briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
 ) : ContactsViewModel(
-    contactManager, conversationManager, connectionRegistry, uiExecutor, dbExecutor, lifecycleManager, db, eventBus
+    contactManager, conversationManager, connectionRegistry, briarExecutors, lifecycleManager, db, eventBus
 ) {
 
     companion object {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
index edff9df35df560a8d13e145651ab2fda83f75007..f47505f6872e77ccb7717723c4cac6b3a85061b6 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -7,7 +7,6 @@ import org.briarproject.bramble.api.contact.ContactId
 import org.briarproject.bramble.api.contact.ContactManager
 import org.briarproject.bramble.api.contact.event.ContactAddedEvent
 import org.briarproject.bramble.api.contact.event.ContactRemovedEvent
-import org.briarproject.bramble.api.db.DatabaseExecutor
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
@@ -17,20 +16,18 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.desktop.utils.removeFirst
 import org.briarproject.briar.desktop.utils.replaceFirst
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
-import org.briarproject.briar.desktop.viewmodel.UiExecutor
-import java.util.concurrent.Executor
 
 abstract class ContactsViewModel(
     protected val contactManager: ContactManager,
     private val conversationManager: ConversationManager,
     private val connectionRegistry: ConnectionRegistry,
-    @UiExecutor uiExecutor: Executor,
-    @DatabaseExecutor dbExecutor: Executor,
+    briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
-) : EventListenerDbViewModel(uiExecutor, dbExecutor, lifecycleManager, db, eventBus) {
+) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
 
     companion object {
         private val LOG = KotlinLogging.logger {}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index e8fd64aad690255b0631d4f4db29cc10faa9822a..c5b962b40ef8a86ecc6ce2227d88c9b3aff4fdfe 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -10,7 +10,6 @@ import org.briarproject.bramble.api.connection.ConnectionRegistry
 import org.briarproject.bramble.api.contact.ContactId
 import org.briarproject.bramble.api.contact.ContactManager
 import org.briarproject.bramble.api.contact.event.ContactRemovedEvent
-import org.briarproject.bramble.api.db.DatabaseExecutor
 import org.briarproject.bramble.api.db.DbException
 import org.briarproject.bramble.api.db.NoSuchContactException
 import org.briarproject.bramble.api.db.TransactionManager
@@ -36,10 +35,9 @@ import org.briarproject.briar.desktop.contact.ContactItem
 import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration
 import org.briarproject.briar.desktop.utils.replaceIf
 import org.briarproject.briar.desktop.utils.replaceIfIndexed
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
-import org.briarproject.briar.desktop.viewmodel.UiExecutor
 import java.util.Date
-import java.util.concurrent.Executor
 import javax.inject.Inject
 
 class ConversationViewModel
@@ -50,12 +48,11 @@ constructor(
     private val conversationManager: ConversationManager,
     private val messagingManager: MessagingManager,
     private val privateMessageFactory: PrivateMessageFactory,
-    @UiExecutor uiExecutor: Executor,
-    @DatabaseExecutor dbExecutor: Executor,
+    briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     private val eventBus: EventBus,
-) : EventListenerDbViewModel(uiExecutor, dbExecutor, lifecycleManager, db, eventBus) {
+) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
 
     companion object {
         private val LOG = KotlinLogging.logger {}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
index 4dbbc93e42e0e5ab2712030cffa9a09613636b1e..376aa46dc13e2c3a9805447939d433297d0ffae0 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
@@ -4,15 +4,13 @@ import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
 import org.briarproject.bramble.api.connection.ConnectionRegistry
 import org.briarproject.bramble.api.contact.ContactManager
-import org.briarproject.bramble.api.db.DatabaseExecutor
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.desktop.contact.ContactItem
 import org.briarproject.briar.desktop.contact.ContactsViewModel
-import org.briarproject.briar.desktop.viewmodel.UiExecutor
-import java.util.concurrent.Executor
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
 import javax.inject.Inject
 
 class IntroductionViewModel
@@ -21,13 +19,12 @@ constructor(
     contactManager: ContactManager,
     conversationManager: ConversationManager,
     connectionRegistry: ConnectionRegistry,
-    @UiExecutor uiExecutor: Executor,
-    @DatabaseExecutor dbExecutor: Executor,
+    briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
 ) : ContactsViewModel(
-    contactManager, conversationManager, connectionRegistry, uiExecutor, dbExecutor, lifecycleManager, db, eventBus
+    contactManager, conversationManager, connectionRegistry, briarExecutors, lifecycleManager, db, eventBus
 ) {
 
     private val _firstContact = mutableStateOf<ContactItem?>(null)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutors.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutors.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3ef59afced267e446421734bed212bb35cfbb992
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutors.kt
@@ -0,0 +1,12 @@
+package org.briarproject.briar.desktop.viewmodel
+
+import org.briarproject.bramble.api.db.DatabaseExecutor
+import org.briarproject.bramble.api.lifecycle.IoExecutor
+
+interface BriarExecutors {
+    fun onDbThread(@DatabaseExecutor task: () -> Unit)
+
+    fun onUiThread(@UiExecutor task: () -> Unit)
+
+    fun onIoThread(@IoExecutor task: () -> Unit)
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutorsImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutorsImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e4170489ba39930676458723f3af8499ecbfd3c9
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/BriarExecutorsImpl.kt
@@ -0,0 +1,20 @@
+package org.briarproject.briar.desktop.viewmodel
+
+import org.briarproject.bramble.api.db.DatabaseExecutor
+import org.briarproject.bramble.api.lifecycle.IoExecutor
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+class BriarExecutorsImpl
+@Inject
+constructor(
+    @UiExecutor private val uiExecutor: Executor,
+    @DatabaseExecutor private val dbExecutor: Executor,
+    @IoExecutor private val ioExecutor: Executor,
+) : BriarExecutors {
+    override fun onDbThread(@DatabaseExecutor task: () -> Unit) = dbExecutor.execute(task)
+
+    override fun onUiThread(@UiExecutor task: () -> Unit) = uiExecutor.execute(task)
+
+    override fun onIoThread(@IoExecutor task: () -> Unit) = ioExecutor.execute(task)
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
index 8c4c5d361d893d22cdcf3fb8c98b833796ccb20a..ebd446878c1fd5bcc9b09e437e75404c0fa0f9fc 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
@@ -6,16 +6,13 @@ import org.briarproject.bramble.api.db.DbCallable
 import org.briarproject.bramble.api.db.DbException
 import org.briarproject.bramble.api.db.DbRunnable
 import org.briarproject.bramble.api.db.TransactionManager
-import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
-import java.util.concurrent.Executor
 
 abstract class DbViewModel(
-    @UiExecutor private val uiExecutor: Executor,
-    @DatabaseExecutor private val dbExecutor: Executor,
+    private val briarExecutors: BriarExecutors,
     private val lifecycleManager: LifecycleManager,
     private val db: TransactionManager
-) : ViewModel, EventListener {
+) : ViewModel {
 
     companion object {
         private val LOG = KotlinLogging.logger {}
@@ -29,7 +26,7 @@ abstract class DbViewModel(
      * For convenience, consider using [runOnDbThreadWithTransaction] instead.
      */
     protected fun runOnDbThread(task: Runnable) {
-        dbExecutor.execute {
+        briarExecutors.onDbThread {
             try {
                 lifecycleManager.waitForDatabase()
                 task.run()
@@ -51,7 +48,7 @@ abstract class DbViewModel(
         task: DbRunnable<Exception>,
         @UiExecutor onError: (Exception) -> Unit
     ) {
-        dbExecutor.execute {
+        briarExecutors.onDbThread {
             try {
                 lifecycleManager.waitForDatabase()
                 db.transaction(readOnly, task)
@@ -59,7 +56,7 @@ abstract class DbViewModel(
                 LOG.warn("Interrupted while waiting for database")
                 Thread.currentThread().interrupt()
             } catch (e: Exception) {
-                uiExecutor.execute { onError(e) }
+                briarExecutors.onUiThread { onError(e) }
             }
         }
     }
@@ -76,7 +73,7 @@ abstract class DbViewModel(
         @UiExecutor onResult: (T) -> Unit,
         @UiExecutor onError: (Exception) -> Unit
     ) {
-        dbExecutor.execute {
+        briarExecutors.onDbThread {
             try {
                 lifecycleManager.waitForDatabase()
                 db.transaction<Exception>(true) { txn ->
@@ -87,7 +84,7 @@ abstract class DbViewModel(
                 LOG.warn("Interrupted while waiting for database")
                 Thread.currentThread().interrupt()
             } catch (e: Exception) {
-                uiExecutor.execute { onError(e) }
+                briarExecutors.onUiThread { onError(e) }
             }
         }
     }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/EventListenerDbViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/EventListenerDbViewModel.kt
index 166d6ff3ff4167e4c159abf08120e01caf1f6954..cff983c625584a03e310bd48db61900742ec25aa 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/EventListenerDbViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/EventListenerDbViewModel.kt
@@ -1,19 +1,16 @@
 package org.briarproject.briar.desktop.viewmodel
 
-import org.briarproject.bramble.api.db.DatabaseExecutor
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
-import java.util.concurrent.Executor
 
 abstract class EventListenerDbViewModel(
-    @UiExecutor uiExecutor: Executor,
-    @DatabaseExecutor dbExecutor: Executor,
+    briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     private val eventBus: EventBus
-) : EventListener, DbViewModel(uiExecutor, dbExecutor, lifecycleManager, db) {
+) : EventListener, DbViewModel(briarExecutors, lifecycleManager, db) {
 
     override fun onInit() {
         eventBus.addListener(this)
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index 121ed1a98396050c0f5a66de6dc76688c8301d55..2e779186ae647894339762124385af0df447ed45 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -35,6 +35,8 @@ import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
+import org.briarproject.briar.desktop.viewmodel.BriarExecutors
+import org.briarproject.briar.desktop.viewmodel.BriarExecutorsImpl
 import org.briarproject.briar.desktop.viewmodel.UiExecutor
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
 import org.briarproject.briar.test.TestModule
@@ -88,6 +90,10 @@ internal class DesktopTestModule(
     @UiExecutor
     fun provideUiExecutor(): Executor = Dispatchers.Swing.asExecutor()
 
+    @Provides
+    @Singleton
+    fun provideBriarExecutors(briarExecutors: BriarExecutorsImpl): BriarExecutors = briarExecutors
+
     @Provides
     @TorDirectory
     internal fun provideTorDirectory(): File {