diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
index d1c225ff6ad834c587d9c52958888cc0241b7085..b8bbd6427bf2df9bddd4c7e88eb52311a116e3cd 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
@@ -52,4 +52,17 @@ interface Database {
     @Throws(DbException::class)
     fun removeContact(txn: Connection, id: Int)
 
+    /**
+     * Runs the given task within a transaction.
+     */
+    @Throws(DbException::class)
+    fun transaction(readOnly: Boolean, task: (Connection) -> Unit)
+
+    /**
+     * Runs the given task within a transaction and returns the result of the
+     * task.
+     */
+    @Throws(DbException::class)
+    fun <R> transactionWithResult(readOnly: Boolean, task: (Connection) -> R): R
+
 }
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 befc146cbb0cd6f33ef29926c9cb652444e08c1b..249058cb1c8023eada8839776e049283edc67b0d 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
@@ -461,4 +461,35 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         }
     }
 
+    override fun transaction(readOnly: Boolean, task: (Connection) -> Unit) {
+        val txn = startTransaction()
+        var success = false
+        try {
+            task(txn)
+            success = true
+        } finally {
+            if (success) {
+                commitTransaction(txn)
+            } else {
+                abortTransaction(txn)
+            }
+        }
+    }
+
+    override fun <R> transactionWithResult(readOnly: Boolean, task: (Connection) -> R): R {
+        val txn = startTransaction()
+        var success = false
+        try {
+            val result = task(txn)
+            success = true
+            return result
+        } finally {
+            if (success) {
+                commitTransaction(txn)
+            } else {
+                abortTransaction(txn)
+            }
+        }
+    }
+
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ec721b8eebabc6658256f8366e2f9d08aa3efb5
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt
@@ -0,0 +1,25 @@
+package org.briarproject.mailbox.core.settings
+
+import org.briarproject.mailbox.core.db.DbException
+import java.sql.Connection
+
+interface SettingsManager {
+    /**
+     * Returns all settings in the given namespace.
+     */
+    @Throws(DbException::class)
+    fun getSettings(namespace: String): Settings
+
+    /**
+     * Returns all settings in the given namespace.
+     */
+    @Throws(DbException::class)
+    fun getSettings(txn: Connection, namespace: String): Settings
+
+    /**
+     * Merges the given settings with any existing settings in the given
+     * namespace.
+     */
+    @Throws(DbException::class)
+    fun mergeSettings(s: Settings, namespace: String)
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..2c9801d0049e564bc297f3269c2a1dec608ffc0e
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt
@@ -0,0 +1,28 @@
+package org.briarproject.mailbox.core.settings
+
+import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.db.DbException
+import java.sql.Connection
+import javax.annotation.concurrent.Immutable
+import javax.inject.Inject
+
+@Immutable
+internal class SettingsManagerImpl @Inject constructor(private val db: Database) : SettingsManager {
+
+    @Throws(DbException::class)
+    override fun getSettings(namespace: String): Settings {
+        return db.transactionWithResult(true) { txn: Connection ->
+            db.getSettings(txn, namespace)
+        }
+    }
+
+    @Throws(DbException::class)
+    override fun getSettings(txn: Connection, namespace: String): Settings {
+        return db.getSettings(txn, namespace)
+    }
+
+    @Throws(DbException::class)
+    override fun mergeSettings(s: Settings, namespace: String) {
+        db.transaction(false) { txn: Connection -> db.mergeSettings(txn, s, namespace) }
+    }
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..75490e09ff388f8b27c6bd5873d244c7cdbea4fd
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt
@@ -0,0 +1,13 @@
+package org.briarproject.mailbox.core.settings
+
+import dagger.Module
+import dagger.Provides
+import org.briarproject.mailbox.core.db.Database
+
+@Module
+class SettingsModule {
+    @Provides
+    fun provideSettingsManager(db: Database): SettingsManager {
+        return SettingsManagerImpl(db)
+    }
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
index b41ac5c317e00b69e523b35112fb64ca5ee01b89..2a9abd05bca1b569c945e5ae0cc888f6bef197ec 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
@@ -1,7 +1,9 @@
 package org.briarproject.mailbox.core
 
 import dagger.Component
+import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.settings.SettingsManager
 import javax.inject.Singleton
 
 @Singleton
@@ -13,4 +15,6 @@ import javax.inject.Singleton
 interface TestComponent {
     fun injectCoreEagerSingletons(): CoreEagerSingletons
     fun getLifecycleManager(): LifecycleManager
+    fun getSettingsManager(): SettingsManager
+    fun getDatabase(): Database
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
index 38d46db1e289328a5f3f4fe3b4394327f2d043e0..20b9590c42713f4fc9ec144fbc436cdd995ec4db 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
@@ -8,6 +8,7 @@ import org.briarproject.mailbox.core.db.DatabaseConfig
 import org.briarproject.mailbox.core.db.DatabaseModule
 import org.briarproject.mailbox.core.lifecycle.LifecycleModule
 import org.briarproject.mailbox.core.server.WebServerModule
+import org.briarproject.mailbox.core.settings.SettingsModule
 import org.briarproject.mailbox.core.system.Clock
 import java.io.File
 import javax.inject.Singleton
@@ -17,6 +18,7 @@ import javax.inject.Singleton
         LifecycleModule::class,
         DatabaseModule::class,
         WebServerModule::class,
+        SettingsModule::class,
         // no Tor module
     ]
 )
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3b2bfb542ec32e88eb02307334032e797eb4358c
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt
@@ -0,0 +1,49 @@
+package org.briarproject.mailbox.core.settings
+
+import org.briarproject.mailbox.core.DaggerTestComponent
+import org.briarproject.mailbox.core.TestComponent
+import org.briarproject.mailbox.core.TestModule
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.TestInstance.Lifecycle
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+import kotlin.test.assertEquals
+
+@TestInstance(Lifecycle.PER_CLASS)
+class SettingsManagerTest {
+
+    private lateinit var testComponent: TestComponent
+
+    @BeforeAll
+    fun setUp(@TempDir tempDir: File) {
+        testComponent = DaggerTestComponent.builder().testModule(TestModule(tempDir)).build()
+        testComponent.injectCoreEagerSingletons()
+        testComponent.getDatabase().open(null)
+    }
+
+    @Test
+    @Throws(java.lang.Exception::class)
+    open fun testMergeSettings() {
+        val before = Settings()
+        before["foo"] = "bar"
+        before["baz"] = "bam"
+        val update = Settings()
+        update["baz"] = "qux"
+        val merged = Settings()
+        merged["foo"] = "bar"
+        merged["baz"] = "qux"
+
+        val sm: SettingsManager = testComponent.getSettingsManager()
+
+        // store 'before'
+        sm.mergeSettings(before, "namespace")
+        assertEquals(before, sm.getSettings("namespace"))
+
+        // merge 'update'
+        sm.mergeSettings(update, "namespace")
+        assertEquals(merged, sm.getSettings("namespace"))
+    }
+
+}