diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
index 015bbe053c8f303e057b0afa46e94bce10a7520b..72433090a790f17ccfdb390e2c2002382c37f192 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
 import org.briarproject.android.dontkillmelib.DozeHelper
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
+import org.briarproject.mailbox.core.system.AndroidWakeLockManager
 import org.briarproject.mailbox.core.system.DozeWatchdog
 import javax.inject.Inject
 
@@ -20,7 +21,8 @@ class MailboxViewModel @Inject constructor(
     private val dozeHelper: DozeHelper,
     private val dozeWatchdog: DozeWatchdog,
     handle: SavedStateHandle,
-    lifecycleManager: LifecycleManager,
+    private val lifecycleManager: LifecycleManager,
+    private val wakeLockManager: AndroidWakeLockManager,
 ) : AndroidViewModel(app) {
 
     val needToShowDoNotKillMeFragment get() = dozeHelper.needToShowDoNotKillMeFragment(app)
@@ -46,6 +48,13 @@ class MailboxViewModel @Inject constructor(
         MailboxService.stopService(getApplication())
     }
 
+    fun wipe() {
+        wakeLockManager.executeWakefully({
+            lifecycleManager.wipeMailbox()
+            MailboxService.stopService(getApplication())
+        }, "LifecycleWipe")
+    }
+
     fun getAndResetDozeFlag() = dozeWatchdog.andResetDozeFlag
 
     fun updateText(str: String) {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt
index 0f129e57150a358dc79df73ccfd7b709fde568c6..93eaea88bdc8075b073663ba7e9facaaf8e319c5 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt
@@ -23,6 +23,7 @@ class MainFragment : Fragment() {
     private val viewModel: MailboxViewModel by activityViewModels()
     private lateinit var statusTextView: TextView
     private lateinit var startStopButton: Button
+    private lateinit var wipeButton: Button
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -37,6 +38,7 @@ class MainFragment : Fragment() {
         val button = v.findViewById<Button>(R.id.button)
         statusTextView = v.findViewById(R.id.statusTextView)
         startStopButton = v.findViewById(R.id.startStopButton)
+        wipeButton = v.findViewById(R.id.wipeButton)
 
         button.setOnClickListener {
             viewModel.updateText("Tested")
@@ -60,7 +62,7 @@ class MainFragment : Fragment() {
     }
 
     private fun onLifecycleStateChanged(state: LifecycleManager.LifecycleState) = when (state) {
-        LifecycleManager.LifecycleState.STOPPED -> {
+        LifecycleManager.LifecycleState.NOT_STARTED -> {
             statusTextView.text = state.name
             startStopButton.setText(R.string.start)
             startStopButton.setOnClickListener { viewModel.startLifecycle() }
@@ -70,11 +72,14 @@ class MainFragment : Fragment() {
             statusTextView.text = state.name
             startStopButton.setText(R.string.stop)
             startStopButton.setOnClickListener { viewModel.stopLifecycle() }
+            wipeButton.setOnClickListener { viewModel.wipe() }
             startStopButton.isEnabled = true
+            wipeButton.isEnabled = true
         }
         else -> {
             statusTextView.text = state.name
             startStopButton.isEnabled = false
+            wipeButton.isEnabled = false
         }
     }
 
diff --git a/mailbox-android/src/main/res/layout/fragment_main.xml b/mailbox-android/src/main/res/layout/fragment_main.xml
index db4d9c81771f55859d48f429bfebb3ab6a4c5488..845336747fb79bc98b8524532c0a03c0db9ceb6b 100644
--- a/mailbox-android/src/main/res/layout/fragment_main.xml
+++ b/mailbox-android/src/main/res/layout/fragment_main.xml
@@ -46,6 +46,18 @@
 		android:layout_margin="16dp"
 		android:text="@string/start"
 		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintBottom_toTopOf="@+id/wipeButton"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent" />
+
+	<Button
+		android:id="@+id/wipeButton"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_margin="16dp"
+		android:enabled="false"
+		android:text="@string/wipe"
+		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent" />
 
diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml
index 8f99672c52e801d3ffd3b3045ee5c2cbd4463648..b5c7cfce62e900edbc85fda8471af2741b47c631 100644
--- a/mailbox-android/src/main/res/values/strings.xml
+++ b/mailbox-android/src/main/res/values/strings.xml
@@ -5,6 +5,7 @@
 	<string name="notification_mailbox_content">Waiting for messages…</string>
 	<string name="start">Start mailbox</string>
 	<string name="stop">Stop mailbox</string>
+	<string name="wipe">Wipe mailbox</string>
 
 	<!-- TODO: We might want to copy string from don't kill me lib,
 	      so translation memory can auto-translate most of them. -->
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 858b612887bdbf9c345ec20b8718e2a796763e70..2036c4cfea515dc625ccffc74bf03f90fe577256 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
@@ -6,7 +6,8 @@ import org.briarproject.mailbox.core.settings.Settings
 interface Database : TransactionManager {
 
     /**
-     * Opens the database and returns true if the database already existed.
+     * Opens the database and returns true if the database already existed. Existence of the
+     * database is defined as the database files exists and the database contains a valid schema.
      */
     fun open(listener: MigrationListener?): Boolean
 
@@ -18,7 +19,7 @@ interface Database : TransactionManager {
     fun close()
 
     @Throws(DbException::class)
-    fun clearDatabase(txn: Transaction)
+    fun dropAllTablesAndClose()
 
     @Throws(DbException::class)
     fun getSettings(txn: Transaction, namespace: String): Settings
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt
index ae7ec1f9ebe644056b91c04880c7631890fdeb83..6ba18129c44bed4ca64685ea136d7649f5b589a9 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt
@@ -3,18 +3,20 @@ package org.briarproject.mailbox.core.db
 import org.briarproject.mailbox.core.db.JdbcUtils.tryToClose
 import org.briarproject.mailbox.core.system.Clock
 import org.briarproject.mailbox.core.util.IoUtils.isNonEmptyDirectory
+import org.briarproject.mailbox.core.util.LogUtils.info
 import org.briarproject.mailbox.core.util.LogUtils.logFileOrDir
 import org.slf4j.LoggerFactory
 import java.io.File
 import java.sql.Connection
 import java.sql.DriverManager
+import java.sql.ResultSet
 import java.sql.SQLException
 import java.sql.Statement
 import java.util.Properties
 
-class H2Database(
+open class H2Database(
     private val config: DatabaseConfig,
-    val clock: Clock,
+    clock: Clock,
 ) : JdbcDatabase(dbTypes, clock) {
 
     internal companion object {
@@ -35,32 +37,58 @@ class H2Database(
 
     override fun open(listener: MigrationListener?): Boolean {
         val dir = config.getDatabaseDirectory()
-        if (LOG.isInfoEnabled) {
-            LOG.info("Contents of account directory before opening DB:")
-            logFileOrDir(LOG, dir.parentFile)
-        }
-        val reopen = isNonEmptyDirectory(dir)
-        if (LOG.isInfoEnabled) LOG.info("Reopening DB: $reopen")
-        if (!reopen && dir.mkdirs()) LOG.info("Created database directory")
-        super.open("org.h2.Driver", reopen, listener)
-        if (LOG.isInfoEnabled) {
-            LOG.info("Contents of account directory after opening DB:")
-            logFileOrDir(LOG, dir.parentFile)
-        }
+        LOG.info { "Contents of account directory before opening DB:" }
+        logFileOrDir(LOG, dir.parentFile)
+        val databaseDirNonEmpty = isNonEmptyDirectory(dir)
+        if (!databaseDirNonEmpty && dir.mkdirs()) LOG.info("Created database directory")
+        val reopen = super.open("org.h2.Driver", listener)
+        LOG.info { "Contents of account directory after opening DB:" }
+        logFileOrDir(LOG, dir.parentFile)
         return reopen
     }
 
+    override fun databaseHasSettingsTable(): Boolean {
+        return read { txn ->
+            val connection = txn.unbox()
+            var tables: ResultSet? = null
+            try {
+                // Need to check for PUBLIC schema as there is another table called SETTINGS on the
+                // INFORMATION_SCHEMA schema.
+                tables = connection.metaData.getTables(null, "PUBLIC", "SETTINGS", null)
+                // if that query returns any rows, the settings table does exist
+                tables.next()
+            } catch (e: SQLException) {
+                LOG.warn("Error while checking for settings table existence", e)
+                tryToClose(tables, LOG)
+                false
+            }
+        }
+    }
+
     override fun close() {
-        // H2 will close the database when the last connection closes
-        var c: Connection? = null
+        connectionsLock.lock()
         try {
-            c = createConnection()
-            super.closeAllConnections()
-            setDirty(c, false)
-            c.close()
-        } catch (e: SQLException) {
-            tryToClose(c, LOG)
-            throw DbException(e)
+            // This extra check is mainly added for tests where we might have closed the database
+            // already by resetting the database after each test and then the lifecycle manager
+            // tries to close again. However closing an already closed database doesn't make
+            // sense, also in production, so bail out quickly here.
+            // This is important especially after the database has been cleared, because the
+            // settings table is gone and if we allowed the flow to continue further, we would try
+            // to store the dirty flag in the no longer existing settings table.
+            if (closed) return
+            // H2 will close the database when the last connection closes
+            var c: Connection? = null
+            try {
+                c = createConnection()
+                super.closeAllConnections()
+                setDirty(c, false)
+                c.close()
+            } catch (e: SQLException) {
+                tryToClose(c, LOG)
+                throw DbException(e)
+            }
+        } finally {
+            connectionsLock.unlock()
         }
     }
 
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 0d3ffe3bea9b09f39a2f4ad439155f4a688fe0ab..e7c7ec23dc09f7ab4fdd209b0803665eae8df41e 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
@@ -9,6 +9,7 @@ import org.briarproject.mailbox.core.db.DatabaseConstants.Companion.SCHEMA_VERSI
 import org.briarproject.mailbox.core.db.JdbcUtils.tryToClose
 import org.briarproject.mailbox.core.settings.Settings
 import org.briarproject.mailbox.core.system.Clock
+import org.briarproject.mailbox.core.util.LogUtils.info
 import org.briarproject.mailbox.core.util.LogUtils.logDuration
 import org.briarproject.mailbox.core.util.LogUtils.logException
 import org.briarproject.mailbox.core.util.LogUtils.now
@@ -50,7 +51,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         """.trimIndent()
     }
 
-    private val connectionsLock: Lock = ReentrantLock()
+    protected val connectionsLock: Lock = ReentrantLock()
     private val connectionsChanged = connectionsLock.newCondition()
 
     @GuardedBy("connectionsLock")
@@ -60,21 +61,29 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
     private var openConnections = 0
 
     @GuardedBy("connectionsLock")
-    private var closed = false
+    protected var closed = false
 
     @Volatile
     private var wasDirtyOnInitialisation = false
 
     private val lock = ReentrantReadWriteLock(true)
 
-    fun open(driverClass: String, reopen: Boolean, listener: MigrationListener?) {
-        // Load the JDBC driver
+    /*
+     * Returns true if the database already existed
+     */
+    internal fun open(driverClass: String, listener: MigrationListener?): Boolean {
         try {
             Class.forName(driverClass)
         } catch (e: ClassNotFoundException) {
             throw DbException(e)
         }
         // Open the database and create the tables and indexes if necessary
+        LOG.info { "checking for settings table" }
+        val reopen = databaseHasSettingsTable()
+        LOG.info {
+            if (reopen) "settings table found, reopening"
+            else "settings table not found, not reopening"
+        }
         var compact = false
         write { txn ->
             val connection = txn.unbox()
@@ -88,9 +97,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
                 initialiseSettings(connection)
                 false
             }
-            if (LOG.isInfoEnabled) {
-                LOG.info("db dirty? $wasDirtyOnInitialisation")
-            }
+            LOG.info { "db dirty? $wasDirtyOnInitialisation" }
             createIndexes(connection)
         }
         // Compact the database if necessary
@@ -100,13 +107,22 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
             compactAndClose()
             logDuration(LOG, { "Compacting database" }, start)
             // Allow the next transaction to reopen the DB
-            synchronized(connectionsLock) { closed = false }
+            connectionsLock.lock()
+            try {
+                closed = false
+            } finally {
+                connectionsLock.unlock()
+            }
             write { txn ->
                 storeLastCompacted(txn.unbox())
             }
         }
+        return reopen
     }
 
+    @Throws(DbException::class, SQLException::class)
+    protected abstract fun databaseHasSettingsTable(): Boolean
+
     /**
      * Compares the schema version stored in the database with the schema
      * version used by the current code and applies any suitable migrations to
@@ -352,10 +368,18 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
     }
 
     @Throws(DbException::class)
-    override fun clearDatabase(txn: Transaction) {
-        val connection: Connection = txn.unbox()
-        execute(connection, "DELETE FROM settings")
-        execute(connection, "DELETE FROM contacts")
+    override fun dropAllTablesAndClose() {
+        connectionsLock.lock()
+        try {
+            write { txn ->
+                val connection: Connection = txn.unbox()
+                execute(connection, "DROP TABLE settings")
+                execute(connection, "DROP TABLE contacts")
+            }
+            closeAllConnections()
+        } finally {
+            connectionsLock.unlock()
+        }
     }
 
     private fun execute(connection: Connection, sql: String) {
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 05f5db54d1958b3da471223b42128ec6fed7e973..04f52a233148173c32bdecd81d0e121978fa1377 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
@@ -20,12 +20,29 @@ import javax.inject.Inject
 private val LOG = getLogger(FileManager::class.java)
 
 class FileManager @Inject constructor(
+    private val fileProvider: FileProvider,
+) {
+    fun deleteAllFiles(): Boolean {
+        var allDeleted = true
+        fileProvider.folderRoot.listFiles()?.forEach { folder ->
+            if (!folder.deleteRecursively()) {
+                allDeleted = false
+                LOG.warn("Not everything in $folder could get deleted.")
+            }
+        } ?: run {
+            allDeleted = false
+            LOG.warn("Could not delete folders.")
+        }
+        return allDeleted
+    }
+}
+
+class FileRouteManager @Inject constructor(
     private val db: Database,
     private val authManager: AuthManager,
     private val fileProvider: FileProvider,
     private val randomIdManager: RandomIdManager,
 ) {
-
     /**
      * Used by contacts to send files to the owner and by the owner to send files to contacts.
      *
@@ -137,20 +154,6 @@ class FileManager @Inject constructor(
         }
         call.respond(folderListResponse)
     }
-
-    fun deleteAllFiles(): Boolean {
-        var allDeleted = true
-        fileProvider.folderRoot.listFiles()?.forEach { folder ->
-            if (!folder.deleteRecursively()) {
-                allDeleted = false
-                LOG.warn("Not everything in $folder could get deleted.")
-            }
-        } ?: run {
-            allDeleted = false
-            LOG.warn("Could not delete folders.")
-        }
-        return allDeleted
-    }
 }
 
 data class FileListResponse(val files: List<FileResponse>)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
index 27654404856566c46e62b7e2322f3ff60a982019..f79710f7559ab2d681468d483b57e71457ae6eac 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
@@ -30,13 +30,15 @@ public interface LifecycleManager {
 	 */
 	enum LifecycleState {
 
-		STOPPED,
+		NOT_STARTED,
 		STARTING,
 		MIGRATING_DATABASE,
 		COMPACTING_DATABASE,
 		STARTING_SERVICES,
 		RUNNING,
-		STOPPING;
+		WIPING,
+		STOPPING,
+		STOPPED;
 
 		public boolean isAfter(LifecycleState state) {
 			return ordinal() > state.ordinal();
@@ -77,6 +79,15 @@ public interface LifecycleManager {
 	@Wakeful
 	void stopServices();
 
+	/**
+	 * Wipes entire database as well as stored files. Also stops all services
+	 * by launching stopServices() on a new thread after wiping completes.
+	 *
+	 * @return true if wiping was successful, false otherwise
+	 */
+	@Wakeful
+	boolean wipeMailbox();
+
 	/**
 	 * Waits for the {@link Database} to be opened before returning.
 	 */
@@ -106,6 +117,9 @@ public interface LifecycleManager {
 		/**
 		 * Called when the database is being opened, before
 		 * {@link #waitForDatabase()} returns.
+		 * <p>
+		 * Don't call any methods from the {@link LifecycleManager} here as
+		 * this is most likely going to end up in a deadlock.
 		 */
 		@Wakeful
 		void onDatabaseOpened(Transaction txn);
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
index 0435916693ded6383645c9c6ae7554f9ad8e52c3..60616eed08d93b64397e0ebaa7af55684c0a6c40 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
@@ -7,16 +7,19 @@ import org.briarproject.mailbox.core.db.MigrationListener
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.NOT_STARTED
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.RUNNING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPED
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPING
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.WIPING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS
+import org.briarproject.mailbox.core.setup.WipeManager
 import org.briarproject.mailbox.core.util.LogUtils.info
 import org.briarproject.mailbox.core.util.LogUtils.logDuration
 import org.briarproject.mailbox.core.util.LogUtils.logException
@@ -27,11 +30,16 @@ import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Semaphore
+import javax.annotation.concurrent.GuardedBy
 import javax.annotation.concurrent.ThreadSafe
 import javax.inject.Inject
+import kotlin.concurrent.thread
 
 @ThreadSafe
-internal class LifecycleManagerImpl @Inject constructor(private val db: Database) :
+internal class LifecycleManagerImpl @Inject constructor(
+    private val db: Database,
+    private val wipeManager: WipeManager,
+) :
     LifecycleManager, MigrationListener {
 
     companion object {
@@ -41,11 +49,15 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database
     private val services: MutableList<Service>
     private val openDatabaseHooks: MutableList<OpenDatabaseHook>
     private val executors: MutableList<ExecutorService>
-    private val startStopSemaphore = Semaphore(1)
+
+    // This semaphore makes sure that startServices(), stopServices() and wipeMailbox()
+    // do not run concurrently. Also all write access to 'state' must happen while this
+    // semaphore is being held.
+    private val startStopWipeSemaphore = Semaphore(1)
     private val dbLatch = CountDownLatch(1)
     private val startupLatch = CountDownLatch(1)
     private val shutdownLatch = CountDownLatch(1)
-    private val state = MutableStateFlow(STOPPED)
+    private val state = MutableStateFlow(NOT_STARTED)
 
     init {
         services = CopyOnWriteArrayList()
@@ -68,12 +80,13 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database
         executors.add(e)
     }
 
+    @GuardedBy("startStopWipeSemaphore")
     override fun startServices(): StartResult {
-        if (!startStopSemaphore.tryAcquire()) {
+        if (!startStopWipeSemaphore.tryAcquire()) {
             LOG.info("Already starting or stopping")
             return ALREADY_RUNNING
         }
-        state.compareAndSet(STOPPED, STARTING)
+        state.compareAndSet(NOT_STARTED, STARTING)
         return try {
             LOG.info("Opening database")
             var start = now()
@@ -101,26 +114,37 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database
             logException(LOG, e)
             SERVICE_ERROR
         } finally {
-            startStopSemaphore.release()
+            startStopWipeSemaphore.release()
         }
     }
 
+    // startStopWipeSemaphore is being held during this because it will be called during db.open()
+    // in startServices()
+    @GuardedBy("startStopWipeSemaphore")
     override fun onDatabaseMigration() {
         state.value = MIGRATING_DATABASE
     }
 
+    // startStopWipeSemaphore is being held during this because it will be called during db.open()
+    // in startServices()
+    @GuardedBy("startStopWipeSemaphore")
     override fun onDatabaseCompaction() {
         state.value = COMPACTING_DATABASE
     }
 
+    @GuardedBy("startStopWipeSemaphore")
     override fun stopServices() {
         try {
-            startStopSemaphore.acquire()
+            startStopWipeSemaphore.acquire()
         } catch (e: InterruptedException) {
             LOG.warn("Interrupted while waiting to stop services")
             return
         }
         try {
+            if (state.value == STOPPED) {
+                return
+            }
+            val wiped = state.value == WIPING
             LOG.info("Stopping services")
             state.value = STOPPING
             for (s in services) {
@@ -132,15 +156,54 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database
                 LOG.trace { "Stopping executor ${e.javaClass.simpleName}" }
                 e.shutdownNow()
             }
-            val start = now()
-            db.close()
-            logDuration(LOG, { "Closing database" }, start)
+            if (wiped) {
+                // If we just wiped, the database has already been closed, so we should not call
+                // close(). Since the services are being shut down after wiping (so that the web
+                // server can still respond to a wipe request), it is possible that a concurrent
+                // API call created some files in the meantime. To make sure we delete those in
+                // case of a wipe, repeat deletion of files here after the services have been
+                // stopped.
+                wipeManager.wipe(wipeDatabase = false)
+            } else {
+                val start = now()
+                db.close()
+                logDuration(LOG, { "Closing database" }, start)
+            }
             shutdownLatch.countDown()
         } catch (e: ServiceException) {
             logException(LOG, e)
         } finally {
-            startStopSemaphore.release()
             state.compareAndSet(STOPPING, STOPPED)
+            startStopWipeSemaphore.release()
+        }
+    }
+
+    @GuardedBy("startStopWipeSemaphore")
+    override fun wipeMailbox(): Boolean {
+        try {
+            startStopWipeSemaphore.acquire()
+        } catch (e: InterruptedException) {
+            LOG.warn("Interrupted while waiting to wipe mailbox")
+            return false
+        }
+        if (!state.compareAndSet(RUNNING, WIPING)) {
+            return false
+        }
+        try {
+            wipeManager.wipe(wipeDatabase = true)
+
+            // We need to move this to a thread so that the webserver call can finish when it calls
+            // this. Otherwise we'll end up in a deadlock: the same thread trying to stop the
+            // webserver from within a call that wants to send a response on the very same webserver.
+            // If we were not do this, the webserver would wait for the request to finish and the
+            // request would wait for the webserver to finish.
+            thread {
+                stopServices()
+            }
+
+            return true
+        } finally {
+            startStopWipeSemaphore.release()
         }
     }
 
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
index aa83f0172ae07e6d95200a98135538347f0a4cf8..d9d92a47aecfdbdf6164e583d8ce2eff2a429c1b 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
@@ -20,10 +20,10 @@ import io.ktor.routing.route
 import io.ktor.routing.routing
 import io.ktor.util.getOrFail
 import org.briarproject.mailbox.core.contacts.ContactsManager
-import org.briarproject.mailbox.core.files.FileManager
+import org.briarproject.mailbox.core.files.FileRouteManager
 import org.briarproject.mailbox.core.settings.MetadataRouteManager
 import org.briarproject.mailbox.core.setup.SetupRouteManager
-import org.briarproject.mailbox.core.setup.WipeManager
+import org.briarproject.mailbox.core.setup.WipeRouteManager
 import org.briarproject.mailbox.core.system.InvalidIdException
 
 internal const val V = "/" // TODO set to "/v1" for release
@@ -31,7 +31,7 @@ internal const val V = "/" // TODO set to "/v1" for release
 internal fun Application.configureBasicApi(
     metadataRouteManager: MetadataRouteManager,
     setupRouteManager: SetupRouteManager,
-    wipeManager: WipeManager,
+    wipeRouteManager: WipeRouteManager,
 ) = routing {
     route(V) {
         get {
@@ -49,7 +49,7 @@ internal fun Application.configureBasicApi(
             }
             delete {
                 call.handle {
-                    wipeManager.onWipeRequest(call)
+                    wipeRouteManager.onWipeRequest(call)
                 }
             }
             put("/setup") {
@@ -85,18 +85,18 @@ internal fun Application.configureContactApi(contactsManager: ContactsManager) =
         }
     }
 
-internal fun Application.configureFilesApi(fileManager: FileManager) = routing {
+internal fun Application.configureFilesApi(fileRouteManager: FileRouteManager) = routing {
 
     authenticate {
         route("$V/files/{folderId}") {
             post {
                 call.handle {
-                    fileManager.postFile(call, call.parameters.getOrFail("folderId"))
+                    fileRouteManager.postFile(call, call.parameters.getOrFail("folderId"))
                 }
             }
             get {
                 call.handle {
-                    fileManager.listFiles(call, call.parameters.getOrFail("folderId"))
+                    fileRouteManager.listFiles(call, call.parameters.getOrFail("folderId"))
                 }
             }
             route("/{fileId}") {
@@ -104,14 +104,14 @@ internal fun Application.configureFilesApi(fileManager: FileManager) = routing {
                     val folderId = call.parameters.getOrFail("folderId")
                     val fileId = call.parameters.getOrFail("fileId")
                     call.handle {
-                        fileManager.getFile(call, folderId, fileId)
+                        fileRouteManager.getFile(call, folderId, fileId)
                     }
                 }
                 delete {
                     val folderId = call.parameters.getOrFail("folderId")
                     val fileId = call.parameters.getOrFail("fileId")
                     call.handle {
-                        fileManager.deleteFile(call, folderId, fileId)
+                        fileRouteManager.deleteFile(call, folderId, fileId)
                     }
                 }
             }
@@ -120,7 +120,7 @@ internal fun Application.configureFilesApi(fileManager: FileManager) = routing {
     authenticate {
         get("$V/folders") {
             call.handle {
-                fileManager.listFoldersWithFiles(call)
+                fileRouteManager.listFoldersWithFiles(call)
             }
         }
     }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
index 5b5ce78539a57b495dd68329eb35753c43bf1afd..2ee5f0095b353ead8fc228249b38ed5227311f2c 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
@@ -9,12 +9,12 @@ import io.ktor.jackson.jackson
 import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
 import org.briarproject.mailbox.core.contacts.ContactsManager
-import org.briarproject.mailbox.core.files.FileManager
+import org.briarproject.mailbox.core.files.FileRouteManager
 import org.briarproject.mailbox.core.lifecycle.Service
 import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
 import org.briarproject.mailbox.core.settings.MetadataRouteManager
 import org.briarproject.mailbox.core.setup.SetupRouteManager
-import org.briarproject.mailbox.core.setup.WipeManager
+import org.briarproject.mailbox.core.setup.WipeRouteManager
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory.getLogger
 import javax.inject.Inject
@@ -32,8 +32,8 @@ internal class WebServerManagerImpl @Inject constructor(
     private val metadataRouteManager: MetadataRouteManager,
     private val setupRouteManager: SetupRouteManager,
     private val contactsManager: ContactsManager,
-    private val fileManager: FileManager,
-    private val wipeManager: WipeManager,
+    private val fileRouteManager: FileRouteManager,
+    private val wipeRouteManager: WipeRouteManager,
 ) : WebServerManager {
 
     internal companion object {
@@ -57,9 +57,9 @@ internal class WebServerManagerImpl @Inject constructor(
                     enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
                 }
             }
-            configureBasicApi(metadataRouteManager, setupRouteManager, wipeManager)
+            configureBasicApi(metadataRouteManager, setupRouteManager, wipeRouteManager)
             configureContactApi(contactsManager)
-            configureFilesApi(fileManager)
+            configureFilesApi(fileRouteManager)
         }
     }
 
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 c87f4ca2bc9a1a681f850538c8f02420d8f36e98..1c0d648588decbe62a4111bed4628503a5b24cd0 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
@@ -5,16 +5,39 @@ import io.ktor.auth.principal
 import io.ktor.http.HttpStatusCode
 import io.ktor.response.respond
 import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.db.DatabaseConfig
 import org.briarproject.mailbox.core.files.FileManager
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.server.AuthException
 import org.briarproject.mailbox.core.server.MailboxPrincipal
+import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal
+import org.briarproject.mailbox.core.util.IoUtils
 import javax.inject.Inject
 
 class WipeManager @Inject constructor(
     private val db: Database,
+    private val databaseConfig: DatabaseConfig,
     private val fileManager: FileManager,
 ) {
 
+    /*
+     * This must only be called by the LifecycleManager
+     */
+    fun wipe(wipeDatabase: Boolean) {
+        if (wipeDatabase) {
+            db.dropAllTablesAndClose()
+            val dir = databaseConfig.getDatabaseDirectory()
+            IoUtils.deleteFileOrDir(dir)
+        }
+        fileManager.deleteAllFiles()
+    }
+
+}
+
+class WipeRouteManager @Inject constructor(
+    private val lifecycleManager: LifecycleManager,
+) {
+
     /**
      * Handler for `DELETE /` API endpoint.
      *
@@ -24,12 +47,13 @@ class WipeManager @Inject constructor(
     @Throws(AuthException::class)
     suspend fun onWipeRequest(call: ApplicationCall) {
         val principal = call.principal<MailboxPrincipal>()
-        if (principal !is MailboxPrincipal.OwnerPrincipal) throw AuthException()
+        if (principal !is OwnerPrincipal) throw AuthException()
 
-        db.write { txn ->
-            db.clearDatabase(txn)
+        val wiped = lifecycleManager.wipeMailbox()
+        if (!wiped) {
+            call.respond(HttpStatusCode.InternalServerError)
+            return
         }
-        fileManager.deleteAllFiles()
 
         call.respond(HttpStatusCode.NoContent)
     }
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 539a39dd52a7eb10a632275c5145fc22fc01ef66..e0ad498eb2764b4fd53066fb45a2cbf2f7feda97 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
@@ -2,6 +2,7 @@ package org.briarproject.mailbox.core
 
 import dagger.Component
 import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.db.DatabaseConfig
 import org.briarproject.mailbox.core.files.FileManager
 import org.briarproject.mailbox.core.files.FileProvider
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
@@ -23,6 +24,7 @@ interface TestComponent {
     fun getSettingsManager(): SettingsManager
     fun getSetupManager(): SetupManager
     fun getFileManager(): FileManager
+    fun getDatabaseConfig(): DatabaseConfig
     fun getDatabase(): Database
     fun getRandomIdManager(): RandomIdManager
     fun getFileProvider(): FileProvider
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 9b270b45e2c69a0a5783fc7659c1fff0d1b7b140..2a1adc06bdf8bc09bb97f6dd91fae266dd1cdf42 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
@@ -5,7 +5,7 @@ import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.components.SingletonComponent
 import org.briarproject.mailbox.core.db.DatabaseConfig
-import org.briarproject.mailbox.core.db.DatabaseModule
+import org.briarproject.mailbox.core.db.TestDatabaseModule
 import org.briarproject.mailbox.core.files.FileProvider
 import org.briarproject.mailbox.core.lifecycle.LifecycleModule
 import org.briarproject.mailbox.core.server.WebServerModule
@@ -17,7 +17,7 @@ import javax.inject.Singleton
 @Module(
     includes = [
         LifecycleModule::class,
-        DatabaseModule::class,
+        TestDatabaseModule::class,
         WebServerModule::class,
         SettingsModule::class,
         // no Tor module
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 acfd7ef9d5d97fa331ad47552471735e476861bf..ffa34af39653988f58da63d9a0d8849287791240 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
@@ -18,7 +18,6 @@ import org.briarproject.mailbox.core.TestUtils.assertJson
 import org.briarproject.mailbox.core.TestUtils.assertTimestampRecent
 import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
 import org.briarproject.mailbox.core.server.IntegrationTest
-import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import kotlin.math.max
@@ -28,19 +27,11 @@ import kotlin.test.assertEquals
 class ContactsManagerIntegrationTest : IntegrationTest() {
 
     @BeforeEach
-    fun initDb() {
+    override fun initDb() {
+        super.initDb()
         addOwnerToken()
     }
 
-    @AfterEach
-    fun clearDb() {
-        db.write { txn ->
-            db.clearDatabase(txn)
-            // clears [metadataManager.ownerConnectionTime]
-            metadataManager.onDatabaseOpened(txn)
-        }
-    }
-
     @Test
     fun `get contacts is initially empty`(): Unit = runBlocking {
         assertEquals(0L, metadataManager.ownerConnectionTime.value)
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 677fe04001cf36a224278c5d23c49004725e85ea..0f7456792197b2a6bd14f3f0ec549b1f4dc470c5 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
@@ -11,7 +11,6 @@ import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.TestUtils
 import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
 import org.briarproject.mailbox.core.server.IntegrationTest
-import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import kotlin.random.Random
@@ -20,18 +19,11 @@ import kotlin.test.assertEquals
 class ContactsManagerMalformedInputIntegrationTest : IntegrationTest(false) {
 
     @BeforeEach
-    fun initDb() {
+    override fun initDb() {
+        super.initDb()
         addOwnerToken()
     }
 
-    @AfterEach
-    fun clearDb() {
-        val db = testComponent.getDatabase()
-        db.write { txn ->
-            db.clearDatabase(txn)
-        }
-    }
-
     /**
      * This test is the same as the one from [ContactsManagerIntegrationTest], just that it supplies
      * raw JSON as a body. Unlike all other tests in this class, this one should be able to create
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1ea56cebc077cdfcfa8c26a30c162772f1c61858
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt
@@ -0,0 +1,25 @@
+package org.briarproject.mailbox.core.db
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.briarproject.mailbox.core.system.Clock
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal class TestDatabaseModule {
+
+    @Provides
+    @Singleton
+    fun provideDatabase(config: DatabaseConfig, clock: Clock): Database {
+        return TestH2Database(config, clock)
+    }
+
+    @Provides
+    fun provideTransactionManager(db: Database): TransactionManager {
+        return db
+    }
+
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt
new file mode 100644
index 0000000000000000000000000000000000000000..165c75bebe5c3f20afd0821e2c4799eb1759944f
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt
@@ -0,0 +1,23 @@
+package org.briarproject.mailbox.core.db
+
+import org.briarproject.mailbox.core.system.Clock
+
+class TestH2Database(
+    config: DatabaseConfig,
+    clock: Clock,
+) : H2Database(config, clock) {
+
+    /**
+     * A special version of open() for testing that allows reopening a database that has been closed.
+     */
+    override fun open(listener: MigrationListener?): Boolean {
+        connectionsLock.lock()
+        try {
+            closed = false
+        } finally {
+            connectionsLock.unlock()
+        }
+        return super.open(listener)
+    }
+
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt
index 8b838ef95e9252d5df850fe8b8b865a2ed666849..de2f1d0461bd0ded29e70e91c0a353160794caec 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt
@@ -24,20 +24,17 @@ class FileManagerIntegrationTest : IntegrationTest() {
     private val bytes = Random.nextBytes(2048)
 
     @BeforeEach
-    fun initDb() {
+    override fun initDb() {
+        super.initDb()
         addOwnerToken()
         addContact(contact1)
         addContact(contact2)
     }
 
     @AfterEach
-    fun cleanUp() {
+    override fun clearDb() {
+        super.clearDb()
         testComponent.getFileManager().deleteAllFiles()
-        db.write { txn ->
-            db.clearDatabase(txn)
-            // clears [metadataManager.ownerConnectionTime]
-            metadataManager.onDatabaseOpened(txn)
-        }
     }
 
     @Test
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt
similarity index 93%
rename from mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt
rename to mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt
index 49cba822da140b9eaead72788b1b1986d2acf890..4a89977ea39b6c4cc51dccf8c948bfb30538a505 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt
@@ -27,14 +27,14 @@ import kotlin.random.Random
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
-class FileManagerTest {
+class FileRouteManagerTest {
 
     private val db: Database = mockk()
     private val authManager: AuthManager = mockk()
     private val fileProvider: FileProvider = mockk()
     private val randomIdManager = RandomIdManager()
 
-    private val fileManager = FileManager(db, authManager, fileProvider, randomIdManager)
+    private val fileRouteManager = FileRouteManager(db, authManager, fileProvider, randomIdManager)
 
     private val call: ApplicationCall = mockk()
     private val id = getNewRandomId()
@@ -58,7 +58,7 @@ class FileManagerTest {
         every { fileProvider.getFile(id, any()) } returns finalFile
         coEvery { call.respond(HttpStatusCode.OK) } just Runs
 
-        fileManager.postFile(call, id)
+        fileRouteManager.postFile(call, id)
 
         assertFalse(tmpFile.exists())
         assertTrue(finalFile.exists())
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 d07a815ceec4fb13cff9791f73413cebec95d63d..10afd983ebd1403e75c3a426035ff35d0a80efdb 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
@@ -17,7 +17,9 @@ import org.briarproject.mailbox.core.TestUtils.getNewRandomId
 import org.briarproject.mailbox.core.contacts.Contact
 import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
 import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.TestInstance
 import org.junit.jupiter.api.TestInstance.Lifecycle
 import org.junit.jupiter.api.io.TempDir
@@ -63,6 +65,21 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) {
         lifecycleManager.waitForShutdown()
     }
 
+    @BeforeEach
+    open fun initDb() {
+        // need to reopen database here because we're closing it after each test
+        db.open(null)
+        db.read { txn ->
+            // clears [metadataManager.ownerConnectionTime]
+            metadataManager.onDatabaseOpened(txn)
+        }
+    }
+
+    @AfterEach
+    open fun clearDb() {
+        db.dropAllTablesAndClose()
+    }
+
     protected fun addOwnerToken() {
         testComponent.getSetupManager().setToken(null, ownerToken)
     }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt
index 2e1a25b145e48abdc628e3ff583de98748f9cbd8..726a6f8cbf6cd9ce343b04c04cdc60e5e64de7d4 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt
@@ -6,7 +6,6 @@ import io.ktor.client.statement.readText
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.server.IntegrationTest
-import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
@@ -14,19 +13,13 @@ import kotlin.test.assertEquals
 class MetadataRouteManagerTest : IntegrationTest() {
 
     @BeforeEach
-    fun initDb() {
+    override fun initDb() {
+        super.initDb()
         addOwnerToken()
         addContact(contact1)
         addContact(contact2)
     }
 
-    @AfterEach
-    fun clearDb() {
-        db.write { txn ->
-            db.clearDatabase(txn)
-        }
-    }
-
     @Test
     fun `owner can access status`(): Unit = runBlocking {
         val response: HttpResponse = httpClient.get("$baseUrl/status") {
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 9f28cace4fb3edff0e37cce31ccb319a9c9ea7cc..83e40b887e92326ca36b829f853641d6726cbe9a 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
@@ -5,7 +5,6 @@ import io.ktor.client.statement.HttpResponse
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.server.IntegrationTest
-import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
@@ -15,16 +14,6 @@ class SetupManagerTest : IntegrationTest() {
 
     private val setupManager by lazy { testComponent.getSetupManager() }
 
-    @AfterEach
-    fun resetToken() {
-        db.write { txn ->
-            // re-set both token to not interfere with other tests
-            db.clearDatabase(txn)
-            // clears [metadataManager.ownerConnectionTime]
-            metadataManager.onDatabaseOpened(txn)
-        }
-    }
-
     @Test
     fun `restarting setup wipes owner token and creates setup token`() {
         // initially, there's no setup and no owner token
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6991e98dd4dbc0544bae03f3327974455a46f447
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt
@@ -0,0 +1,31 @@
+package org.briarproject.mailbox.core.setup
+
+import io.ktor.client.request.delete
+import io.ktor.client.statement.HttpResponse
+import io.ktor.http.HttpStatusCode
+import kotlinx.coroutines.runBlocking
+import org.briarproject.mailbox.core.server.IntegrationTest
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+
+class WipeRouteManagerTest : IntegrationTest() {
+
+    @Test
+    fun `wipe request rejects non-owners`() = runBlocking {
+        addOwnerToken()
+        addContact(contact1)
+
+        // Unauthorized with random token
+        val response1 = httpClient.delete<HttpResponse>("$baseUrl/") {
+            authenticateWithToken(token)
+        }
+        assertEquals(HttpStatusCode.Unauthorized, response1.status)
+
+        // Unauthorized with contact's token
+        val response2 = httpClient.delete<HttpResponse>("$baseUrl/") {
+            authenticateWithToken(contact1.token)
+        }
+        assertEquals(HttpStatusCode.Unauthorized, response2.status)
+    }
+
+}
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/WipingWipeRouteManagerTest.kt
similarity index 54%
rename from mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt
rename to mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt
index b7c606099c6626550ebb86c3e5187b336011c0aa..984af156d0059376039d3ce0b0d83397210c3857 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/WipingWipeRouteManagerTest.kt
@@ -5,32 +5,17 @@ import io.ktor.client.request.post
 import io.ktor.client.statement.HttpResponse
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
+import org.briarproject.mailbox.core.db.DbException
 import org.briarproject.mailbox.core.server.IntegrationTest
 import org.junit.jupiter.api.Test
 import kotlin.random.Random
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
 
-class WipeManagerTest : IntegrationTest() {
-
-    @Test
-    fun `wipe request rejects non-owners`() = runBlocking {
-        addOwnerToken()
-        addContact(contact1)
-
-        // Unauthorized with random token
-        val response1 = httpClient.delete<HttpResponse>("$baseUrl/") {
-            authenticateWithToken(token)
-        }
-        assertEquals(HttpStatusCode.Unauthorized, response1.status)
-
-        // Unauthorized with contact's token
-        val response2 = httpClient.delete<HttpResponse>("$baseUrl/") {
-            authenticateWithToken(contact1.token)
-        }
-        assertEquals(HttpStatusCode.Unauthorized, response2.status)
-    }
+class WipingWipeRouteManagerTest : IntegrationTest() {
 
     @Test
     fun `wipe request deletes files and db for owner`() = runBlocking {
@@ -51,19 +36,40 @@ class WipeManagerTest : IntegrationTest() {
         }
         assertEquals(HttpStatusCode.NoContent, response.status)
 
-        // no more contacts in DB
-        val contacts = db.read { db.getContacts(it) }
-        assertEquals(0, contacts.size)
-
-        // owner token was cleared as well
-        val token = db.read { txn ->
-            testComponent.getSetupManager().getOwnerToken(txn)
-        }
-        assertNull(token)
+        // assert that database is gone
+        assertFalse(testComponent.getDatabaseConfig().getDatabaseDirectory().exists())
 
         // no more files are stored
         val folderRoot = testComponent.getFileProvider().folderRoot
         assertTrue(folderRoot.listFiles()?.isEmpty() ?: false)
+
+        // no more contacts in DB - contacts table is gone
+        // it actually fails because db is closed though
+        assertFailsWith<DbException> { db.read { db.getContacts(it) } }
+
+        // owner token was cleared as well - settings table is gone
+        // it actually fails because db is closed though
+        assertFailsWith<DbException> {
+            db.read { txn ->
+                testComponent.getSetupManager().getOwnerToken(txn)
+            }
+        }
+
+        // re-open the database
+        db.open(null)
+
+        // reopening re-created the database directory
+        assertTrue(testComponent.getDatabaseConfig().getDatabaseDirectory().exists())
+
+        // no more contacts in DB
+        assertTrue(db.read { db.getContacts(it) }.isEmpty())
+
+        // owner token was cleared as well
+        assertNull(
+            db.read { txn ->
+                testComponent.getSetupManager().getOwnerToken(txn)
+            }
+        )
     }
 
 }