diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle
index 323ed59f4861aa1e5b7837f55b3476e9779b806a..e4868ed5433f1be1acc8ab0076ce6b49abb0870d 100644
--- a/mailbox-android/build.gradle
+++ b/mailbox-android/build.gradle
@@ -57,9 +57,10 @@ dependencies {
     implementation "androidx.activity:activity-ktx:1.3.1"
     implementation "androidx.fragment:fragment-ktx:1.3.6"
 
-    def lifecycle_version = "2.3.1"
+    def lifecycle_version = "2.4.0-alpha03"
     implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
     implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
     implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
     implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
 
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt
index a8788fa9413f743ae53caff854effe8d6f0c5735..fcc52e4bb31b5d0bd926b045235cdfe80b2b1a56 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt
@@ -15,9 +15,4 @@ class MailboxApplication : MultiDexApplication() {
     @Inject
     internal lateinit var androidEagerSingletons: AndroidEagerSingletons
 
-    override fun onCreate() {
-        super.onCreate()
-        MailboxService.startService(this)
-    }
-
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
index 57c2f8260b592e0c74ce899bfe6ff4be4d9e3d8c..8ff1823fb288124e5a52df349298d701c5bcd04b 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
@@ -1,8 +1,10 @@
 package org.briarproject.mailbox.android
 
 import android.app.Service
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.content.IntentFilter
 import android.os.IBinder
 import androidx.core.content.ContextCompat
 import dagger.hilt.android.AndroidEntryPoint
@@ -13,8 +15,10 @@ 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.SUCCESS
 import org.slf4j.LoggerFactory.getLogger
+import java.util.concurrent.atomic.AtomicBoolean
 
 import javax.inject.Inject
+import kotlin.system.exitProcess
 
 @AndroidEntryPoint
 class MailboxService : Service() {
@@ -33,8 +37,11 @@ class MailboxService : Service() {
         }
     }
 
+    private val created = AtomicBoolean(false)
+
     @Volatile
     internal var started = false
+    private var receiver: BroadcastReceiver? = null
 
     @Inject
     internal lateinit var wakeLockManager: AndroidWakeLockManager
@@ -48,24 +55,48 @@ class MailboxService : Service() {
     override fun onCreate() {
         super.onCreate()
 
+        LOG.info("Created")
+        if (created.getAndSet(true)) {
+            LOG.warn("Already created")
+            // FIXME when can this happen? Next line will kill app
+            stopSelf()
+            return
+        }
+
         // Hold a wake lock during startup
         wakeLockManager.runWakefully({
             startForeground(NOTIFICATION_MAIN_ID, notificationManager.serviceNotification)
             // Start the services in a background thread
             wakeLockManager.executeWakefully({
                 val result: StartResult = lifecycleManager.startServices()
-                if (result === SUCCESS) {
-                    started = true
-                } else if (result === ALREADY_RUNNING) {
-                    LOG.info("Already running")
-                    stopSelf()
-                } else {
-                    if (LOG.isWarnEnabled) LOG.warn("Startup failed: $result")
-                    // TODO: implement this
-                    // showStartupFailure(result)
-                    stopSelf()
+                when {
+                    result === SUCCESS -> started = true
+                    result === ALREADY_RUNNING -> {
+                        LOG.warn("Already running")
+                        // FIXME when can this happen? Next line will kill app
+                        stopSelf()
+                    }
+                    else -> {
+                        if (LOG.isWarnEnabled) LOG.warn("Startup failed: $result")
+                        // TODO: implement this
+                        //  and start activity in new process, so we can kill this one
+                        // showStartupFailure(result)
+                        stopSelf()
+                    }
                 }
             }, "LifecycleStartup")
+            // Register for device shutdown broadcasts
+            receiver = object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    LOG.info("Device is shutting down")
+                    stopSelf()
+                }
+            }
+            val filter = IntentFilter()
+            filter.addAction(Intent.ACTION_SHUTDOWN)
+            filter.addAction("android.intent.action.QUICKBOOT_POWEROFF")
+            filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF")
+            registerReceiver(receiver, filter)
         }, "LifecycleStartup")
     }
 
@@ -76,10 +107,21 @@ class MailboxService : Service() {
     override fun onDestroy() {
         wakeLockManager.runWakefully({
             super.onDestroy()
-            wakeLockManager.executeWakefully(
-                { lifecycleManager.stopServices() },
-                "LifecycleShutdown"
-            )
+            LOG.info("Destroyed")
+            stopForeground(true)
+            if (receiver != null) unregisterReceiver(receiver)
+            wakeLockManager.executeWakefully({
+                try {
+                    if (started) {
+                        lifecycleManager.stopServices()
+                        lifecycleManager.waitForShutdown()
+                    }
+                } catch (e: InterruptedException) {
+                    LOG.info("Interrupted while waiting for shutdown")
+                }
+                LOG.info("Exiting")
+                exitProcess(0)
+            }, "LifecycleShutdown")
         }, "LifecycleShutdown")
     }
 }
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 9ca00b30f7441bae9d11dc2df903950ff3e1f0e7..3dd9c84dcab8dd1997df7a3fe0d59c09bc8e9d15 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
@@ -5,17 +5,31 @@ import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.SavedStateHandle
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.StateFlow
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
 import javax.inject.Inject
 
 @HiltViewModel
 class MailboxViewModel @Inject constructor(
     app: Application,
     handle: SavedStateHandle,
+    lifecycleManager: LifecycleManager,
 ) : AndroidViewModel(app) {
 
     private val _text = handle.getLiveData("text", "Hello Mailbox")
     val text: LiveData<String> = _text
 
+    val lifecycleState: StateFlow<LifecycleState> = lifecycleManager.lifecycleStateFlow
+
+    fun startLifecycle() {
+        MailboxService.startService(getApplication())
+    }
+
+    fun stopLifecycle() {
+        MailboxService.stopService(getApplication())
+    }
+
     fun updateText(str: String) {
         _text.value = str
     }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt
index 6d338040768cf0d36560aeef7fd41ac63d427e86..679e69accb50dff7bd9c761633a790ffe3a4e461 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt
@@ -5,13 +5,21 @@ import android.widget.Button
 import android.widget.TextView
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
 import org.briarproject.mailbox.R
+import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
 
 @AndroidEntryPoint
 class MainActivity : AppCompatActivity() {
 
     private val viewModel: MailboxViewModel by viewModels()
+    private lateinit var statusTextView: TextView
+    private lateinit var startStopButton: Button
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -19,13 +27,46 @@ class MainActivity : AppCompatActivity() {
 
         val textView = findViewById<TextView>(R.id.text)
         val button = findViewById<Button>(R.id.button)
+        statusTextView = findViewById(R.id.statusTextView)
+        startStopButton = findViewById(R.id.startStopButton)
 
         button.setOnClickListener {
             viewModel.updateText("Tested")
         }
 
+        // Start a coroutine in the lifecycle scope
+        lifecycleScope.launch {
+            // repeatOnLifecycle launches the block in a new coroutine every time the
+            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Trigger the flow and start listening for values.
+                // Note that this happens when lifecycle is STARTED and stops
+                // collecting when the lifecycle is STOPPED
+                viewModel.lifecycleState.collect { onLifecycleStateChanged(it) }
+            }
+        }
+
         viewModel.text.observe(this, { text ->
             textView.text = text
         })
     }
+
+    private fun onLifecycleStateChanged(state: LifecycleState) = when (state) {
+        LifecycleState.STOPPED -> {
+            statusTextView.text = state.name
+            startStopButton.setText(R.string.start)
+            startStopButton.setOnClickListener { viewModel.startLifecycle() }
+            startStopButton.isEnabled = true
+        }
+        LifecycleState.RUNNING -> {
+            statusTextView.text = state.name
+            startStopButton.setText(R.string.stop)
+            startStopButton.setOnClickListener { viewModel.stopLifecycle() }
+            startStopButton.isEnabled = true
+        }
+        else -> {
+            statusTextView.text = state.name
+            startStopButton.isEnabled = false
+        }
+    }
 }
diff --git a/mailbox-android/src/main/res/layout/activity_main.xml b/mailbox-android/src/main/res/layout/activity_main.xml
index 62bd8a4a775e0859a8e5bab58d0384860b85a1fe..26cce69f629360bc5c346f67b7cce544aed60c4a 100644
--- a/mailbox-android/src/main/res/layout/activity_main.xml
+++ b/mailbox-android/src/main/res/layout/activity_main.xml
@@ -26,4 +26,27 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/text" />
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+    <TextView
+        android:id="@+id/statusTextView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:gravity="center"
+        app:layout_constraintBottom_toTopOf="@+id/startStopButton"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/button"
+        app:layout_constraintVertical_bias="1.0"
+        tools:text="STOPPED" />
+
+    <Button
+        android:id="@+id/startStopButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:text="@string/start"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml
index 141a92ef8d2445e795e88c458af8431458542414..580b932af496ebd4ab45620215b1420d5d30a8b8 100644
--- a/mailbox-android/src/main/res/values/strings.xml
+++ b/mailbox-android/src/main/res/values/strings.xml
@@ -3,4 +3,6 @@
     <string name="notification_channel_name">Briar Mailbox Channel</string>
     <string name="notification_mailbox_title">Briar Mailbox running</string>
     <string name="notification_mailbox_content">Waiting for messages…</string>
+    <string name="start">Start mailbox</string>
+    <string name="stop">Stop mailbox</string>
 </resources>
\ No newline at end of file
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 8bf4e72483e842b7b12b10cf7f433ae6c07bd9f4..63719354388f2fd8af55660a4e5ddf476b9219a8 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
@@ -5,6 +5,8 @@ import org.briarproject.mailbox.core.system.Wakeful;
 
 import java.util.concurrent.ExecutorService;
 
+import kotlinx.coroutines.flow.StateFlow;
+
 /**
  * Manages the lifecycle of the app: opening and closing the
  * {@link DatabaseComponent} starting and stopping {@link Service Services},
@@ -27,7 +29,7 @@ public interface LifecycleManager {
      */
     enum LifecycleState {
 
-        STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES,
+        STOPPED, STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES,
         RUNNING, STOPPING;
 
         public boolean isAfter(LifecycleState state) {
@@ -92,6 +94,8 @@ public interface LifecycleManager {
      */
     LifecycleState getLifecycleState();
 
+    StateFlow<LifecycleState> getLifecycleStateFlow();
+
     interface OpenDatabaseHook {
         /**
          * Called when the database is being opened, before
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java
deleted file mode 100644
index 6bd84052dfbc0304e90b6a3fb6d04699be9d50d3..0000000000000000000000000000000000000000
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package org.briarproject.mailbox.core.lifecycle;
-
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.RUNNING;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPING;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR;
-import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS;
-import static org.briarproject.mailbox.core.util.LogUtils.info;
-import static org.briarproject.mailbox.core.util.LogUtils.logDuration;
-import static org.briarproject.mailbox.core.util.LogUtils.logException;
-import static org.briarproject.mailbox.core.util.LogUtils.now;
-import static org.briarproject.mailbox.core.util.LogUtils.trace;
-import static org.slf4j.LoggerFactory.getLogger;
-
-import org.briarproject.mailbox.core.db.DatabaseComponent;
-import org.briarproject.mailbox.core.db.MigrationListener;
-import org.slf4j.Logger;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Semaphore;
-
-import javax.annotation.concurrent.ThreadSafe;
-import javax.inject.Inject;
-
-@ThreadSafe
-class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
-
-    private static final Logger LOG = getLogger(LifecycleManagerImpl.class);
-
-    private final DatabaseComponent db;
-    private final List<Service> services;
-    private final List<OpenDatabaseHook> openDatabaseHooks;
-    private final List<ExecutorService> executors;
-    private final Semaphore startStopSemaphore = new Semaphore(1);
-    private final CountDownLatch dbLatch = new CountDownLatch(1);
-    private final CountDownLatch startupLatch = new CountDownLatch(1);
-    private final CountDownLatch shutdownLatch = new CountDownLatch(1);
-
-    private volatile LifecycleState state = STARTING;
-
-    @Inject
-    LifecycleManagerImpl(DatabaseComponent db) {
-        this.db = db;
-        services = new CopyOnWriteArrayList<>();
-        openDatabaseHooks = new CopyOnWriteArrayList<>();
-        executors = new CopyOnWriteArrayList<>();
-    }
-
-    @Override
-    public void registerService(Service s) {
-        info(LOG, () -> "Registering service " + s.getClass().getSimpleName());
-        services.add(s);
-    }
-
-    @Override
-    public void registerOpenDatabaseHook(OpenDatabaseHook hook) {
-        info(LOG, () -> "Registering open database hook " + hook.getClass().getSimpleName());
-        openDatabaseHooks.add(hook);
-    }
-
-    @Override
-    public void registerForShutdown(ExecutorService e) {
-        info(LOG, () -> "Registering executor " + e.getClass().getSimpleName());
-        executors.add(e);
-    }
-
-    @Override
-    public StartResult startServices() {
-        if (!startStopSemaphore.tryAcquire()) {
-            LOG.info("Already starting or stopping");
-            return ALREADY_RUNNING;
-        }
-        try {
-            LOG.info("Opening database");
-            long start = now();
-            boolean reopened = db.open(this);
-            if (reopened) logDuration(LOG, () -> "Reopening database", start);
-            else logDuration(LOG, () -> "Creating database", start);
-
-            LOG.info("Starting services");
-            state = STARTING_SERVICES;
-            dbLatch.countDown();
-
-            for (Service s : services) {
-                start = now();
-                s.startService();
-                logDuration(LOG, () -> "Starting service " + s.getClass().getSimpleName(), start);
-            }
-
-            state = RUNNING;
-            startupLatch.countDown();
-            return SUCCESS;
-        } catch (ServiceException e) {
-            logException(LOG, e);
-            return SERVICE_ERROR;
-        } finally {
-            startStopSemaphore.release();
-        }
-    }
-
-    @Override
-    public void onDatabaseMigration() {
-        state = MIGRATING_DATABASE;
-    }
-
-    @Override
-    public void onDatabaseCompaction() {
-        state = COMPACTING_DATABASE;
-    }
-
-    @Override
-    public void stopServices() {
-        try {
-            startStopSemaphore.acquire();
-        } catch (InterruptedException e) {
-            LOG.warn("Interrupted while waiting to stop services");
-            return;
-        }
-        try {
-            LOG.info("Stopping services");
-            state = STOPPING;
-            for (Service s : services) {
-                long start = now();
-                s.stopService();
-                logDuration(LOG, () -> "Stopping service " + s.getClass().getSimpleName(), start);
-            }
-            for (ExecutorService e : executors) {
-                trace(LOG, () -> "Stopping executor " + e.getClass().getSimpleName());
-                e.shutdownNow();
-            }
-            long start = now();
-            db.close();
-            logDuration(LOG, () -> "Closing database", start);
-            shutdownLatch.countDown();
-        } catch (ServiceException e) {
-            logException(LOG, e);
-        } finally {
-            startStopSemaphore.release();
-        }
-    }
-
-    @Override
-    public void waitForDatabase() throws InterruptedException {
-        dbLatch.await();
-    }
-
-    @Override
-    public void waitForStartup() throws InterruptedException {
-        startupLatch.await();
-    }
-
-    @Override
-    public void waitForShutdown() throws InterruptedException {
-        shutdownLatch.await();
-    }
-
-    @Override
-    public LifecycleState getLifecycleState() {
-        return state;
-    }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..d5e659d0c5c455419179f82bb52ea561e8edd515
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
@@ -0,0 +1,163 @@
+package org.briarproject.mailbox.core.lifecycle
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.briarproject.mailbox.core.db.DatabaseComponent
+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.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.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.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
+import org.briarproject.mailbox.core.util.LogUtils.trace
+import org.slf4j.LoggerFactory.getLogger
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Semaphore
+import javax.annotation.concurrent.ThreadSafe
+import javax.inject.Inject
+
+@ThreadSafe
+internal class LifecycleManagerImpl @Inject constructor(private val db: DatabaseComponent) :
+    LifecycleManager, MigrationListener {
+
+    companion object {
+        private val LOG = getLogger(LifecycleManagerImpl::class.java)
+    }
+
+    private val services: MutableList<Service>
+    private val openDatabaseHooks: MutableList<OpenDatabaseHook>
+    private val executors: MutableList<ExecutorService>
+    private val startStopSemaphore = Semaphore(1)
+    private val dbLatch = CountDownLatch(1)
+    private val startupLatch = CountDownLatch(1)
+    private val shutdownLatch = CountDownLatch(1)
+    private val state = MutableStateFlow(STOPPED)
+
+    init {
+        services = CopyOnWriteArrayList()
+        openDatabaseHooks = CopyOnWriteArrayList()
+        executors = CopyOnWriteArrayList()
+    }
+
+    override fun registerService(s: Service) {
+        LOG.info { "Registering service ${s.javaClass.simpleName}" }
+        services.add(s)
+    }
+
+    override fun registerOpenDatabaseHook(hook: OpenDatabaseHook) {
+        LOG.info { "Registering open database hook ${hook.javaClass.simpleName}" }
+        openDatabaseHooks.add(hook)
+    }
+
+    override fun registerForShutdown(e: ExecutorService) {
+        LOG.info { "Registering executor ${e.javaClass.simpleName}" }
+        executors.add(e)
+    }
+
+    override fun startServices(): StartResult {
+        if (!startStopSemaphore.tryAcquire()) {
+            LOG.info("Already starting or stopping")
+            return ALREADY_RUNNING
+        }
+        state.compareAndSet(STOPPED, STARTING)
+        return try {
+            LOG.info("Opening database")
+            var start = now()
+            val reopened = db.open(this)
+            if (reopened) logDuration(LOG, { "Reopening database" }, start)
+            else logDuration(LOG, { "Creating database" }, start)
+            LOG.info("Starting services")
+            state.value = STARTING_SERVICES
+            dbLatch.countDown()
+            for (s in services) {
+                start = now()
+                s.startService()
+                logDuration(LOG, { "Starting service  ${s.javaClass.simpleName}" }, start)
+            }
+            state.compareAndSet(STARTING_SERVICES, RUNNING)
+            startupLatch.countDown()
+            SUCCESS
+        } catch (e: ServiceException) {
+            logException(LOG, e)
+            SERVICE_ERROR
+        } finally {
+            startStopSemaphore.release()
+        }
+    }
+
+    override fun onDatabaseMigration() {
+        state.value = MIGRATING_DATABASE
+    }
+
+    override fun onDatabaseCompaction() {
+        state.value = COMPACTING_DATABASE
+    }
+
+    override fun stopServices() {
+        try {
+            startStopSemaphore.acquire()
+        } catch (e: InterruptedException) {
+            LOG.warn("Interrupted while waiting to stop services")
+            return
+        }
+        try {
+            LOG.info("Stopping services")
+            state.value = STOPPING
+            for (s in services) {
+                val start = now()
+                s.stopService()
+                logDuration(LOG, { "Stopping service " + s.javaClass.simpleName }, start)
+            }
+            for (e in executors) {
+                LOG.trace { "Stopping executor ${e.javaClass.simpleName}" }
+                e.shutdownNow()
+            }
+            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)
+        }
+    }
+
+    @Throws(InterruptedException::class)
+    override fun waitForDatabase() {
+        dbLatch.await()
+    }
+
+    @Throws(InterruptedException::class)
+    override fun waitForStartup() {
+        startupLatch.await()
+    }
+
+    @Throws(InterruptedException::class)
+    override fun waitForShutdown() {
+        shutdownLatch.await()
+    }
+
+    override fun getLifecycleState(): LifecycleState {
+        return state.value
+    }
+
+    override fun getLifecycleStateFlow(): StateFlow<LifecycleState> {
+        return state
+    }
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
index 9f73da2e5f9c37927d75a873a763c889b4a34fdb..db5edc719efc6abb447868d29c4fcfbd181bf7aa 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
@@ -26,6 +26,7 @@ import net.freehaven.tor.control.TorControlConnection;
 import org.briarproject.mailbox.core.PoliteExecutor;
 import org.briarproject.mailbox.core.lifecycle.Service;
 import org.briarproject.mailbox.core.lifecycle.ServiceException;
+import org.briarproject.mailbox.core.server.WebServerManager;
 import org.briarproject.mailbox.core.system.Clock;
 import org.briarproject.mailbox.core.system.LocationUtils;
 import org.briarproject.mailbox.core.system.ResourceProvider;
@@ -215,7 +216,7 @@ abstract class TorPlugin implements Service, EventHandler {
         // Check whether we're online
         updateConnectionStatus(networkManager.getNetworkStatus());
         // Create a hidden service if necessary
-        ioExecutor.execute(() -> publishHiddenService("8888"));
+        ioExecutor.execute(() -> publishHiddenService(String.valueOf(WebServerManager.PORT)));
     }
 
     private boolean assetsAreUpToDate() {
@@ -585,18 +586,6 @@ abstract class TorPlugin implements Service, EventHandler {
 //            callback.pluginStateChanged(getState());
         }
 
-        // Doesn't affect getState()
-        synchronized boolean setServerSocket(ServerSocket ss) {
-            if (stopped || serverSocket != null) return false;
-            serverSocket = ss;
-            return true;
-        }
-
-        // Doesn't affect getState()
-        synchronized void clearServerSocket(ServerSocket ss) {
-            if (serverSocket == ss) serverSocket = null;
-        }
-
         synchronized State getState() {
             if (!started || stopped || !settingsChecked) {
                 return STARTING_STOPPING;