diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index cb2abda0e79e9e6fb4f6ea9125b93a18f0bb2d9f..0a00e4f5685d68d48d6489a23e0a6557cfd1d415 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -8,7 +8,7 @@ import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import org.briarproject.mailbox.core.CoreEagerSingletons
 import org.briarproject.mailbox.core.JavaCliEagerSingletons
-import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.db.TransactionManager
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.setup.QrCodeEncoder
 import org.briarproject.mailbox.core.setup.SetupManager
@@ -42,7 +42,7 @@ class Main : CliktCommand(
     internal lateinit var lifecycleManager: LifecycleManager
 
     @Inject
-    internal lateinit var db: Database
+    internal lateinit var db: TransactionManager
 
     @Inject
     internal lateinit var setupManager: SetupManager
@@ -82,10 +82,10 @@ class Main : CliktCommand(
         lifecycleManager.waitForStartup()
 
         // TODO this is obviously not the final code, just a stub to get us started
-        val setupTokenExists = db.transactionWithResult(true) { txn ->
+        val setupTokenExists = db.read { txn ->
             setupManager.getSetupToken(txn) != null
         }
-        val ownerTokenExists = db.transactionWithResult(true) { txn ->
+        val ownerTokenExists = db.read { txn ->
             setupManager.getOwnerToken(txn) != null
         }
         if (!setupTokenExists && !ownerTokenExists) setupManager.restartSetup()
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/ContactsManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/ContactsManager.kt
index 240a6b5c69107632327c30a7833815c98d569407..e4e9bd422ab16780e127946b14fac02b848788c4 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/ContactsManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/ContactsManager.kt
@@ -34,7 +34,7 @@ class ContactsManager @Inject constructor(
     suspend fun listContacts(call: ApplicationCall) {
         authManager.assertIsOwner(call.principal())
 
-        val contacts = db.transactionWithResult(true) { txn ->
+        val contacts = db.read { txn ->
             db.getContacts(txn)
         }
         val contactIds = contacts.map { contact -> contact.contactId }
@@ -60,7 +60,7 @@ class ContactsManager @Inject constructor(
         randomIdManager.assertIsRandomId(c.inboxId)
         randomIdManager.assertIsRandomId(c.outboxId)
 
-        val status = db.transactionWithResult(false) { txn ->
+        val status = db.write { txn ->
             if (db.getContact(txn, c.contactId) != null) {
                 Conflict
             } else {
@@ -83,7 +83,7 @@ class ContactsManager @Inject constructor(
             throw BadRequestException("Invalid value for parameter contactId")
         }
 
-        val status = db.transactionWithResult(false) { txn ->
+        val status = db.write { txn ->
             if (db.getContact(txn, contactId) == null) {
                 NotFound
             } else {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt
index e32d0fc766b3d5afeac82f95b08fed3652ef22db..31cf523063750485c5663c5a8f3678203d529051 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt
@@ -17,4 +17,9 @@ internal class DatabaseModule {
         return H2Database(config, clock)
     }
 
+    @Provides
+    fun provideTransactionManager(db: Database): TransactionManager {
+        return db
+    }
+
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
index 31c5a8a32ceb0f058db42cb03158b74b0efc6af7..fe61611bcf7ac2f461e05248838b7da3814edbf5 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
@@ -18,7 +18,6 @@ import java.sql.PreparedStatement
 import java.sql.ResultSet
 import java.sql.SQLException
 import java.sql.Statement
-import java.util.Arrays
 import java.util.LinkedList
 import java.util.concurrent.locks.Lock
 import java.util.concurrent.locks.ReentrantLock
@@ -76,7 +75,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         }
         // Open the database and create the tables and indexes if necessary
         var compact = false
-        transaction(false) { txn ->
+        write { txn ->
             val connection = txn.unbox()
             compact = if (reopen) {
                 val s: Settings = getSettings(connection, DB_SETTINGS_NAMESPACE)
@@ -101,7 +100,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
             logDuration(LOG, { "Compacting database" }, start)
             // Allow the next transaction to reopen the DB
             synchronized(connectionsLock) { closed = false }
-            transaction(false) { txn ->
+            write { txn ->
                 storeLastCompacted(txn.unbox())
             }
         }
@@ -149,7 +148,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
 
     @Suppress("MemberVisibilityCanBePrivate") // visible for testing
     internal fun getMigrations(): List<Migration<Connection>> {
-        return Arrays.asList<Migration<Connection>>(
+        return listOf(
             // Migration1_2(dbTypes),
         )
     }
@@ -604,17 +603,19 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         }
     }
 
-    override fun transaction(readOnly: Boolean, task: (Transaction) -> Unit) {
-        val txn = startTransaction(readOnly)
-        try {
-            task(txn)
-            commitTransaction(txn)
-        } finally {
-            endTransaction(txn)
-        }
+    override fun <R> read(task: (Transaction) -> R): R {
+        return transaction(true, task)
+    }
+
+    override fun <R> write(task: (Transaction) -> R): R {
+        return transaction(false, task)
     }
 
-    override fun <R> transactionWithResult(readOnly: Boolean, task: (Transaction) -> R): R {
+    /**
+     * Runs the given task within a transaction and returns the result of the
+     * task.
+     */
+    private fun <R> transaction(readOnly: Boolean, task: (Transaction) -> R): R {
         val txn = startTransaction(readOnly)
         try {
             val result = task(txn)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt
index d663509b2d682e9d163075dd35ea438eb21f3ac8..679457c94b193551dc9ec755be7c6cc4b8e77baf 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt
@@ -3,15 +3,15 @@ package org.briarproject.mailbox.core.db
 interface TransactionManager {
 
     /**
-     * Runs the given task within a transaction.
+     * Runs the given task within a read-only transaction and returns its result.
      */
     @Throws(DbException::class)
-    fun transaction(readOnly: Boolean, task: (Transaction) -> Unit)
+    fun <R> read(task: (Transaction) -> R): R
 
     /**
-     * Runs the given task within a transaction and returns the result of the
-     * task.
+     * Runs the given task within a read/write transaction and returns its result.
      */
     @Throws(DbException::class)
-    fun <R> transactionWithResult(readOnly: Boolean, task: (Transaction) -> R): R
+    fun <R> write(task: (Transaction) -> R): R
+
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
index f38e3af89595ce62d02de84c6ae9018a65a33755..05f5db54d1958b3da471223b42128ec6fed7e973 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
@@ -125,7 +125,7 @@ class FileManager @Inject constructor(
 
         val folderListResponse = withContext(Dispatchers.IO) {
             val list = ArrayList<FolderResponse>()
-            val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) }
+            val contacts = db.read { txn -> db.getContacts(txn) }
             contacts.forEach { c ->
                 val id = c.outboxId
                 val folder = fileProvider.getFolder(id)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
index 3c8301296828eb018621f9eb26fc84e646757b83..0aaf277b5c9bbe07ca044abfd33c1f842ba585b1 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
@@ -24,7 +24,7 @@ class AuthManager @Inject constructor(
      */
     fun getPrincipal(token: String): MailboxPrincipal? {
         randomIdManager.assertIsRandomId(token)
-        return db.transactionWithResult(true) { txn ->
+        return db.read { txn ->
             val contact = db.getContactWithToken(txn, token)
             when {
                 contact != null -> ContactPrincipal(contact)
@@ -44,7 +44,7 @@ class AuthManager @Inject constructor(
         if (principal == null) throw AuthException()
 
         if (principal is OwnerPrincipal) {
-            val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) }
+            val contacts = db.read { txn -> db.getContacts(txn) }
             val noOutboxFound = contacts.none { c -> folderId == c.outboxId }
             if (noOutboxFound) throw AuthException()
         } else if (principal is ContactPrincipal) {
@@ -61,7 +61,7 @@ class AuthManager @Inject constructor(
         if (principal == null) throw AuthException()
 
         if (principal is OwnerPrincipal) {
-            val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) }
+            val contacts = db.read { txn -> db.getContacts(txn) }
             val noInboxFound = contacts.none { c -> folderId == c.inboxId }
             if (noInboxFound) throw AuthException()
         } else if (principal is ContactPrincipal) {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt
index c1ccc66733d9d8efd8f9782d23b742db83d6caf2..59a199d2f3eb50c02ec1edc9614eb6154cbc54ee 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt
@@ -11,7 +11,7 @@ internal class SettingsManagerImpl @Inject constructor(private val db: Database)
 
     @Throws(DbException::class)
     override fun getSettings(namespace: String): Settings {
-        return db.transactionWithResult(true) { txn ->
+        return db.read { txn ->
             db.getSettings(txn, namespace)
         }
     }
@@ -23,6 +23,6 @@ internal class SettingsManagerImpl @Inject constructor(private val db: Database)
 
     @Throws(DbException::class)
     override fun mergeSettings(s: Settings, namespace: String) {
-        db.transaction(false) { txn -> db.mergeSettings(txn, s, namespace) }
+        db.write { txn -> db.mergeSettings(txn, s, namespace) }
     }
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
index 334482d378562efaa5e64ce376ae791b75c8933c..29a47061d7a362064edeaea70155ce69c39b4efe 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
@@ -4,8 +4,8 @@ import com.google.zxing.BarcodeFormat.QR_CODE
 import com.google.zxing.common.BitMatrix
 import com.google.zxing.qrcode.QRCodeWriter
 import dev.keiji.util.Base32
-import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.db.DbException
+import org.briarproject.mailbox.core.db.TransactionManager
 import org.briarproject.mailbox.core.tor.TorPlugin
 import org.briarproject.mailbox.core.util.LogUtils.logException
 import org.briarproject.mailbox.core.util.StringUtils.fromHexString
@@ -18,7 +18,7 @@ private const val VERSION = 32
 private val LOG = getLogger(QrCodeEncoder::class.java)
 
 class QrCodeEncoder @Inject constructor(
-    private val db: Database,
+    private val db: TransactionManager,
     private val setupManager: SetupManager,
     private val torPlugin: TorPlugin,
 ) {
@@ -62,7 +62,7 @@ class QrCodeEncoder @Inject constructor(
 
     private fun getSetupTokenBytes(): ByteArray? {
         val tokenString = try {
-            db.transactionWithResult(true) { txn ->
+            db.read { txn ->
                 setupManager.getSetupToken(txn)
             }
         } catch (e: DbException) {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
index 3da9b2e0cb39188ef58e20f1e2a6996db7210b3d..c87f4ca2bc9a1a681f850538c8f02420d8f36e98 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
@@ -26,7 +26,7 @@ class WipeManager @Inject constructor(
         val principal = call.principal<MailboxPrincipal>()
         if (principal !is MailboxPrincipal.OwnerPrincipal) throw AuthException()
 
-        db.transaction(false) { txn ->
+        db.write { txn ->
             db.clearDatabase(txn)
         }
         fileManager.deleteAllFiles()
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
index 6849f8c32a4b343f9474689da3b5d64183639a30..90b367b0fc48784c38397a24d15bb0b30b2adc8b 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
@@ -7,8 +7,8 @@ import io.ktor.http.HttpStatusCode
 import io.mockk.every
 import io.mockk.mockk
 import org.briarproject.mailbox.core.contacts.Contact
-import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.db.Transaction
+import org.briarproject.mailbox.core.db.TransactionManager
 import org.briarproject.mailbox.core.system.ID_SIZE
 import org.briarproject.mailbox.core.system.toHex
 import org.briarproject.mailbox.core.util.IoUtils
@@ -33,13 +33,32 @@ object TestUtils {
     )
 
     /**
-     * Allows you to mock [Database] access happening within a [Transaction] more comfortably.
-     * Calls to [Database.transactionWithResult] will be mocked.
+     * Allows you to mock [TransactionManager] access
+     * happening within a [Transaction] more comfortably.
+     * Calls to [TransactionManager.read] will be mocked.
      * The given lambda [block] will get captured and invoked.
      */
-    fun <T> everyTransactionWithResult(db: Database, readOnly: Boolean, block: (Transaction) -> T) {
-        val txn = Transaction(mockk(), readOnly)
-        every { db.transactionWithResult<T>(true, captureLambda()) } answers {
+    fun <T> TransactionManager.everyRead(
+        block: (Transaction) -> T,
+    ) {
+        val txn = Transaction(mockk(), true)
+        every { read<T>(captureLambda()) } answers {
+            lambda<(Transaction) -> T>().captured.invoke(txn)
+        }
+        block(txn)
+    }
+
+    /**
+     * Allows you to mock [TransactionManager] access
+     * happening within a [Transaction] more comfortably.
+     * Calls to [TransactionManager.write] will be mocked.
+     * The given lambda [block] will get captured and invoked.
+     */
+    fun <T> TransactionManager.everyWrite(
+        block: (Transaction) -> T,
+    ) {
+        val txn = Transaction(mockk(), true)
+        every { write<T>(captureLambda()) } answers {
             lambda<(Transaction) -> T>().captured.invoke(txn)
         }
         block(txn)
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt
index 767df7596aea2d357c3eab15a6749308166c686e..bb11417d6cd8180373c0f2acbf8cf2e3410a4e54 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt
@@ -35,7 +35,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() {
 
     @AfterEach
     fun clearDb() {
-        db.transaction(false) { txn ->
+        db.write { txn ->
             db.clearDatabase(txn)
         }
     }
@@ -124,7 +124,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() {
         }
         assertJson("""{ "contacts": [ 1, 2, 3 ] }""", response2)
 
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertEquals(c1, db.getContact(txn, 1))
             assertEquals(c2, db.getContact(txn, 2))
             assertEquals(c3, db.getContact(txn, 3))
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt
index e538c35e3a993db7f96d14ffe087f0725d612d80..677fe04001cf36a224278c5d23c49004725e85ea 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt
@@ -27,7 +27,7 @@ class ContactsManagerMalformedInputIntegrationTest : IntegrationTest(false) {
     @AfterEach
     fun clearDb() {
         val db = testComponent.getDatabase()
-        db.transaction(false) { txn ->
+        db.write { txn ->
             db.clearDatabase(txn)
         }
     }
@@ -62,7 +62,7 @@ class ContactsManagerMalformedInputIntegrationTest : IntegrationTest(false) {
         TestUtils.assertJson("""{ "contacts": [ 1, 2, 3 ] }""", response2)
 
         val db = testComponent.getDatabase()
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertEquals(c1, db.getContact(txn, 1))
             assertEquals(c2, db.getContact(txn, 2))
             assertEquals(c3, db.getContact(txn, 3))
@@ -220,7 +220,7 @@ class ContactsManagerMalformedInputIntegrationTest : IntegrationTest(false) {
         TestUtils.assertJson("""{ "contacts": [ 1, 2 ] }""", response2)
 
         val db = testComponent.getDatabase()
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertEquals(c1, db.getContact(txn, 1))
             assertEquals(c2, db.getContact(txn, 2))
         }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
index 67b41d18f3238abd45fa8208c10e17615b04e090..d24afe2de61d1b8c71bbc7e5ea5d2ecb72e40769 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
@@ -52,7 +52,7 @@ abstract class JdbcDatabaseTest {
             outboxId = randomIdManager.getNewRandomId()
         )
         var db: Database = open(false)
-        db.transaction(false) { txn ->
+        db.write { txn ->
 
             db.addContact(txn, contact1)
             db.addContact(txn, contact2)
@@ -61,7 +61,7 @@ abstract class JdbcDatabaseTest {
 
         // Check that the records are still there
         db = open(true)
-        db.transaction(false) { txn ->
+        db.write { txn ->
             val contact1Reloaded1 = db.getContact(txn, 1)
             val contact2Reloaded1 = db.getContact(txn, 2)
             assertEquals(contact1, contact1Reloaded1)
@@ -77,7 +77,7 @@ abstract class JdbcDatabaseTest {
 
         // Check that the record is gone
         db = open(true)
-        db.transaction(true) { txn ->
+        db.read { txn ->
             val contact1Reloaded2 = db.getContact(txn, 1)
             val contact2Reloaded2 = db.getContact(txn, 2)
             assertNull(contact1Reloaded2)
@@ -99,7 +99,7 @@ abstract class JdbcDatabaseTest {
         merged["baz"] = "qux"
 
         val db: Database = open(false)
-        db.transaction(false) { txn ->
+        db.write { txn ->
             // store 'before'
             db.mergeSettings(txn, before, "namespace")
             assertEquals(before, db.getSettings(txn, "namespace"))
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
index 397b11f860268b36d1a928080918db245d66fdc8..4501fcf4ffa037ae38d1bbdac0ca243f5151c1d9 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
@@ -2,7 +2,7 @@ package org.briarproject.mailbox.core.server
 
 import io.mockk.every
 import io.mockk.mockk
-import org.briarproject.mailbox.core.TestUtils.everyTransactionWithResult
+import org.briarproject.mailbox.core.TestUtils.everyRead
 import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
 import org.briarproject.mailbox.core.TestUtils.getNewRandomId
 import org.briarproject.mailbox.core.db.Database
@@ -41,7 +41,7 @@ class AuthManagerTest {
 
     @Test
     fun `getPrincipal() returns authenticated contact`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContactWithToken(txn, id) } returns contactPrincipal.contact
         }
         assertEquals(contactPrincipal, authManager.getPrincipal(id))
@@ -49,7 +49,7 @@ class AuthManagerTest {
 
     @Test
     fun `getPrincipal() returns authenticated owner`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContactWithToken(txn, id) } returns null
             every { setupManager.getOwnerToken(txn) } returns id
         }
@@ -59,7 +59,7 @@ class AuthManagerTest {
 
     @Test
     fun `getPrincipal() returns null when unauthenticated`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContactWithToken(txn, id) } returns null
             every { setupManager.getOwnerToken(txn) } returns otherId
             every { setupManager.getSetupToken(txn) } returns otherId
@@ -70,7 +70,7 @@ class AuthManagerTest {
 
     @Test
     fun `getPrincipal() returns SetupPrincipal`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContactWithToken(txn, id) } returns null
             every { setupManager.getOwnerToken(txn) } returns otherId
             every { setupManager.getSetupToken(txn) } returns id
@@ -88,7 +88,7 @@ class AuthManagerTest {
 
     @Test
     fun `assertCanDownloadFromFolder() throws if owner wants non-existent folder`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContacts(txn) } returns emptyList()
         }
 
@@ -109,7 +109,7 @@ class AuthManagerTest {
 
     @Test
     fun `assertCanDownloadFromFolder() lets owner access contact's outbox folder`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContacts(txn) } returns listOf(contact, getNewRandomContact())
         }
 
@@ -130,7 +130,7 @@ class AuthManagerTest {
 
     @Test
     fun `assertCanPostToFolder() throws if owner wants non-existent folder`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContacts(txn) } returns emptyList()
         }
 
@@ -151,7 +151,7 @@ class AuthManagerTest {
 
     @Test
     fun `assertCanPostToFolder() lets owner access contact's inbox folder`() {
-        everyTransactionWithResult(db, true) { txn ->
+        db.everyRead { txn ->
             every { db.getContacts(txn) } returns listOf(contact, getNewRandomContact())
         }
 
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
index 238b39da86cb3c695f34cbd81a88b9428f083891..54752fac5091fd5ca352b3aa75a29efd6010471f 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
@@ -62,7 +62,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) {
 
     protected fun addContact(c: Contact) {
         val db = testComponent.getDatabase()
-        db.transaction(false) { txn ->
+        db.write { txn ->
             db.addContact(txn, c)
         }
     }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt
index 9ee9c88e122bfa60e57210e25dcb1f64074e3888..aa4ad01edf673a4270f07fc2fdc98565a5c4d6e0 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt
@@ -25,21 +25,21 @@ class SetupManagerTest : IntegrationTest() {
     @Test
     fun `restarting setup wipes owner token and creates setup token`() {
         // initially, there's no setup and no owner token
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertNull(setupManager.getSetupToken(txn))
             assertNull(setupManager.getOwnerToken(txn))
         }
 
         // setting an owner token stores it in DB
         setupManager.setToken(null, ownerToken)
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertNull(setupManager.getSetupToken(txn))
             assertEquals(ownerToken, setupManager.getOwnerToken(txn))
         }
 
         // restarting setup wipes owner token, creates setup token
         setupManager.restartSetup()
-        db.transaction(true) { txn ->
+        db.read { txn ->
             val setupToken = setupManager.getSetupToken(txn)
             assertNotNull(setupToken)
             testComponent.getRandomIdManager().assertIsRandomId(setupToken)
@@ -50,7 +50,7 @@ class SetupManagerTest : IntegrationTest() {
     @Test
     fun `setup request gets rejected when using non-setup token`() = runBlocking {
         // initially, there's no setup and no owner token
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertNull(setupManager.getSetupToken(txn))
             assertNull(setupManager.getOwnerToken(txn))
         }
@@ -91,7 +91,7 @@ class SetupManagerTest : IntegrationTest() {
             authenticateWithToken(token)
         }
         // setup token got wiped and new owner token from response got stored
-        db.transaction(true) { txn ->
+        db.read { txn ->
             assertNull(setupManager.getSetupToken(txn))
             assertEquals(setupManager.getOwnerToken(txn), response.token)
         }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt
index 23fb16a71573106beaa0761d53702ba86e1d6ffc..a4d847aab563c9efce1f22ead183da3ad3d6b634 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt
@@ -54,11 +54,11 @@ class WipeManagerTest : IntegrationTest() {
         assertEquals(HttpStatusCode.NoContent, response.status)
 
         // no more contacts in DB
-        val contacts = db.transactionWithResult(true) { db.getContacts(it) }
+        val contacts = db.read { db.getContacts(it) }
         assertEquals(0, contacts.size)
 
         // owner token was cleared as well
-        val token = db.transactionWithResult(true) { txn ->
+        val token = db.read { txn ->
             testComponent.getSetupManager().getOwnerToken(txn)
         }
         assertNull(token)