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 4250ab95722cfd1c107407e25824fd2f5172e83a..54de6261bec9bc3a1e254fc9a8012a0b569e15a8 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
@@ -82,6 +82,11 @@ public interface LifecycleManager {
 	 */
 	void registerForShutdown(ExecutorService e);
 
+	/**
+	 * Check if the mailbox has been set up already.
+	 */
+	boolean hasBeenSetUp();
+
 	/**
 	 * Opens the {@link Database} using the given key and starts any
 	 * registered {@link Service Services}.
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 ab7a5703f57429de8b0df195699c3cd5fb59e58c..431dcc1486cd51184732ee2cee4b8b57e3c79f6b 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
@@ -35,6 +35,7 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult
 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.SetupManager
 import org.briarproject.mailbox.core.setup.WipeManager
 import org.briarproject.mailbox.core.util.LogUtils.info
 import org.briarproject.mailbox.core.util.LogUtils.logDuration
@@ -54,6 +55,7 @@ import kotlin.concurrent.thread
 @ThreadSafe
 internal class LifecycleManagerImpl @Inject constructor(
     private val db: Database,
+    private val setupManager: SetupManager,
     private val wipeManager: WipeManager,
 ) :
     LifecycleManager, MigrationListener {
@@ -96,12 +98,39 @@ internal class LifecycleManagerImpl @Inject constructor(
         executors.add(e)
     }
 
+    @GuardedBy("startStopWipeSemaphore")
+    override fun hasBeenSetUp(): Boolean {
+        try {
+            startStopWipeSemaphore.acquire()
+        } catch (e: InterruptedException) {
+            LOG.warn("Interrupted while checking whether mailbox has been set up")
+            return false
+        }
+        return try {
+            if (state.value == RUNNING) {
+                // while RUNNING, we can just ask the database
+                db.read { txn -> setupManager.getOwnerToken(txn) != null }
+            } else {
+                // before [startServices] has been called or after [stopServices],
+                // open and close database for looking up owner token
+                db.open(null)
+                try {
+                    db.read { txn -> setupManager.getOwnerToken(txn) != null }
+                } finally {
+                    db.close()
+                }
+            }
+        } finally {
+            startStopWipeSemaphore.release()
+        }
+    }
+
     @GuardedBy("startStopWipeSemaphore")
     override fun startServices(): StartResult {
         try {
             startStopWipeSemaphore.acquire()
         } catch (e: InterruptedException) {
-            LOG.warn("Interrupted while waiting to stop services")
+            LOG.warn("Interrupted while waiting to start services")
             return SERVICE_ERROR
         }
         if (!state.compareAndSet(NOT_STARTED, STARTING)) {
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 10afd983ebd1403e75c3a426035ff35d0a80efdb..da2538c06424327a21b5adfb9ca0f006a12e5c10 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
@@ -30,7 +30,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) {
 
     protected lateinit var testComponent: TestComponent
     protected val db by lazy { testComponent.getDatabase() }
-    private val lifecycleManager by lazy { testComponent.getLifecycleManager() }
+    protected val lifecycleManager by lazy { testComponent.getLifecycleManager() }
     protected val metadataManager by lazy { testComponent.getMetadataManager() }
     protected val httpClient = HttpClient(CIO) {
         expectSuccess = false // prevents exceptions on non-success responses
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
index 6991e98dd4dbc0544bae03f3327974455a46f447..8c63f04f293a49823ab3d1b41967747a0b9677ce 100644
--- 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
@@ -7,6 +7,8 @@ import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.server.IntegrationTest
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
 
 class WipeRouteManagerTest : IntegrationTest() {
 
@@ -28,4 +30,26 @@ class WipeRouteManagerTest : IntegrationTest() {
         assertEquals(HttpStatusCode.Unauthorized, response2.status)
     }
 
+    @Test
+    fun `set up state is consistent`() = runBlocking {
+        assertFalse(lifecycleManager.hasBeenSetUp())
+        addOwnerToken()
+        assertTrue(lifecycleManager.hasBeenSetUp())
+        addContact(contact1)
+        assertTrue(lifecycleManager.hasBeenSetUp())
+
+        // 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)
+        assertTrue(lifecycleManager.hasBeenSetUp())
+    }
+
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest2.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest2.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3835358552ae30da2776a2096a2e8b9a13a36a9a
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest2.kt
@@ -0,0 +1,79 @@
+package org.briarproject.mailbox.core.setup
+
+import io.ktor.client.request.delete
+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 WipingWipeRouteManagerTest2 : IntegrationTest() {
+
+    @Test
+    fun `set up state is consistent`() = runBlocking {
+        assertFalse(lifecycleManager.hasBeenSetUp())
+        addOwnerToken()
+        assertTrue(lifecycleManager.hasBeenSetUp())
+        addContact(contact1)
+        addContact(contact2)
+
+        // owner uploads a file
+        val uploadResponse: HttpResponse = httpClient.post("$baseUrl/files/${contact1.inboxId}") {
+            authenticateWithToken(ownerToken)
+            body = Random.nextBytes(42)
+        }
+        assertEquals(HttpStatusCode.OK, uploadResponse.status)
+
+        // owner wipes mailbox
+        val response = httpClient.delete<HttpResponse>("$baseUrl/") {
+            authenticateWithToken(ownerToken)
+        }
+        assertEquals(HttpStatusCode.NoContent, response.status)
+
+        // assert that database is gone
+        assertFalse(testComponent.getDatabaseConfig().getDatabaseDirectory().exists())
+
+        assertFalse(lifecycleManager.hasBeenSetUp())
+
+        // assert that database has been re-created by checking set-up state
+        assertTrue(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)
+
+        // 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)
+            }
+        )
+    }
+
+}