diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarDesktopApp.kt b/src/main/kotlin/org/briarproject/briar/desktop/BriarDesktopApp.kt
index 4e0ef30e51449ab292acc320f4aad459e5ba71d8..d4dcded3a9005e1514a1f207cca59f0a0fdffb2c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/BriarDesktopApp.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/BriarDesktopApp.kt
@@ -18,7 +18,7 @@ import javax.inject.Singleton
 @Singleton
 internal interface BriarDesktopApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons {
 
-    fun getUI(): UI
+    fun getBriarUi(): BriarUi
 
     fun getSecureRandom(): SecureRandom
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt b/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt
deleted file mode 100644
index 68093730fb08db1c66f87ecb275f017a001649b8..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/BriarService.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-package org.briarproject.briar.desktop
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.window.Window
-import org.briarproject.bramble.api.account.AccountManager
-import org.briarproject.bramble.api.contact.Contact
-import org.briarproject.bramble.api.contact.ContactManager
-import org.briarproject.bramble.api.crypto.DecryptionException
-import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
-import org.briarproject.bramble.api.identity.IdentityManager
-import org.briarproject.bramble.api.lifecycle.LifecycleManager
-import org.briarproject.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.messaging.MessagingManager
-import org.briarproject.briar.desktop.dialogs.Login
-import org.briarproject.briar.desktop.dialogs.Registration
-import org.briarproject.briar.desktop.paul.theme.BriarTheme
-import org.briarproject.briar.desktop.paul.views.BriarUIStateManager
-import java.awt.Dimension
-import java.util.logging.Logger
-import javax.annotation.concurrent.Immutable
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.system.exitProcess
-
-enum class Screen {
-    REGISTRATION,
-    LOGIN,
-    MAIN
-}
-
-interface BriarService {
-    @Composable
-    fun start(
-        contactManager: ContactManager,
-        conversationManager: ConversationManager,
-        messagingManager: MessagingManager,
-        identityManager: IdentityManager,
-    )
-
-    fun stop()
-}
-
-val CVM = compositionLocalOf<ConversationManager> { error("Undefined ConversationManager") }
-val CTM = compositionLocalOf<ContactManager> { error("Undefined ContactManager") }
-val MM = compositionLocalOf<MessagingManager> { error("Undefined MessagingManager") }
-val IM = compositionLocalOf<IdentityManager> { error("Undefined IdentityManager") }
-
-@Immutable
-@Singleton
-internal class BriarServiceImpl
-@Inject
-constructor(
-    private val accountManager: AccountManager,
-    private val contactManager: ContactManager,
-    private val messagingManager: MessagingManager,
-    private val lifecycleManager: LifecycleManager,
-    private val passwordStrengthEstimator: PasswordStrengthEstimator
-) : BriarService {
-
-    companion object {
-        private val LOG = Logger.getLogger(BriarServiceImpl::class.java.name)
-    }
-
-    private val contacts: MutableList<Contact> = ArrayList()
-
-    override fun stop() {
-        lifecycleManager.stopServices()
-        lifecycleManager.waitForShutdown()
-    }
-
-    @Composable
-    override fun start(
-        contactManager: ContactManager,
-        conversationManager: ConversationManager,
-        messagingManager: MessagingManager,
-        identityManager: IdentityManager,
-    ) {
-        val (isDark, setDark) = remember { mutableStateOf(true) }
-        val title = "Briar Desktop"
-        var screenState by remember {
-            mutableStateOf(
-                if (accountManager.hasDatabaseKey()) {
-                    // this should only happen during testing when we launch the main UI directly
-                    // without a need to enter the password.
-                    loadContacts()
-                    Screen.MAIN
-                } else if (accountManager.accountExists()) {
-                    Screen.LOGIN
-                } else {
-                    Screen.REGISTRATION
-                }
-            )
-        }
-        Window(
-            title = title,
-            onCloseRequest = { exitProcess(0) },
-        ) {
-            window.minimumSize = Dimension(800, 600)
-            BriarTheme(isDarkTheme = isDark) {
-                when (screenState) {
-                    Screen.REGISTRATION ->
-                        Registration(
-                            onSubmit = { username, password ->
-                                accountManager.createAccount(username, password)
-                                signedIn()
-                                loadContacts()
-                                screenState = Screen.MAIN
-                            }
-                        )
-                    Screen.LOGIN ->
-                        Login(
-                            onResult = {
-                                try {
-                                    accountManager.signIn(it)
-                                    signedIn()
-                                    loadContacts()
-                                    screenState = Screen.MAIN
-                                } catch (e: DecryptionException) {
-                                    // failure, try again
-                                }
-                            }
-                        )
-
-                    else ->
-                        CompositionLocalProvider(
-                            CVM provides conversationManager,
-                            CTM provides contactManager,
-                            MM provides messagingManager,
-                            IM provides identityManager,
-                        ) {
-                            BriarUIStateManager(contacts, isDark, setDark)
-                        }
-                }
-            }
-        }
-    }
-
-    private fun signedIn() {
-        val dbKey = accountManager.databaseKey ?: throw AssertionError()
-        lifecycleManager.startServices(dbKey)
-        lifecycleManager.waitForStartup()
-    }
-
-    private fun loadContacts() {
-        val contacts = contactManager.contacts
-        for (contact in contacts) {
-            LOG.info("loaded contact: ${contact.author.name} (${contact.alias})")
-            this.contacts.add(contact)
-        }
-    }
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..52f9fa36d23b04765842638344bb7e4ade6d588a
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt
@@ -0,0 +1,126 @@
+package org.briarproject.briar.desktop
+
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import org.briarproject.bramble.api.account.AccountManager
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.identity.IdentityManager
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.messaging.MessagingManager
+import org.briarproject.briar.desktop.dialogs.ContactsViewModel
+import org.briarproject.briar.desktop.dialogs.Login
+import org.briarproject.briar.desktop.dialogs.LoginViewModel
+import org.briarproject.briar.desktop.dialogs.Registration
+import org.briarproject.briar.desktop.dialogs.RegistrationViewModel
+import org.briarproject.briar.desktop.paul.theme.BriarTheme
+import org.briarproject.briar.desktop.paul.views.BriarUIStateManager
+import java.awt.Dimension
+import java.util.logging.Logger
+import javax.annotation.concurrent.Immutable
+import javax.inject.Inject
+import javax.inject.Singleton
+
+enum class Screen {
+    REGISTRATION,
+    LOGIN,
+    MAIN
+}
+
+interface BriarUi {
+
+    fun start()
+
+    fun stop()
+}
+
+val CVM = compositionLocalOf<ConversationManager> { error("Undefined ConversationManager") }
+val CTM = compositionLocalOf<ContactManager> { error("Undefined ContactManager") }
+val MM = compositionLocalOf<MessagingManager> { error("Undefined MessagingManager") }
+val IM = compositionLocalOf<IdentityManager> { error("Undefined IdentityManager") }
+
+@Immutable
+@Singleton
+internal class BriarUiImpl
+@Inject
+constructor(
+    private val registrationViewModel: RegistrationViewModel,
+    private val loginViewModel: LoginViewModel,
+    private val contactsViewModel: ContactsViewModel,
+    private val accountManager: AccountManager,
+    private val contactManager: ContactManager,
+    private val conversationManager: ConversationManager,
+    private val identityManager: IdentityManager,
+    private val messagingManager: MessagingManager,
+    private val lifecycleManager: LifecycleManager,
+) : BriarUi {
+
+    companion object {
+        private val LOG = Logger.getLogger(BriarUiImpl::class.java.name)
+    }
+
+    override fun stop() {
+        // TODO: check how briar is doing this
+        if (lifecycleManager.lifecycleState == RUNNING) {
+            lifecycleManager.stopServices()
+            lifecycleManager.waitForShutdown()
+        }
+    }
+
+    override fun start() {
+        application {
+            val (isDark, setDark) = remember { mutableStateOf(true) }
+            val title = "Briar Desktop"
+            var screenState by remember {
+                mutableStateOf(
+                    if (accountManager.hasDatabaseKey()) {
+                        // this should only happen during testing when we launch the main UI directly
+                        // without a need to enter the password.
+                        contactsViewModel.loadContacts()
+                        Screen.MAIN
+                    } else if (accountManager.accountExists()) {
+                        Screen.LOGIN
+                    } else {
+                        Screen.REGISTRATION
+                    }
+                )
+            }
+            Window(
+                title = title,
+                onCloseRequest = { stop(); exitApplication() },
+            ) {
+                window.minimumSize = Dimension(800, 600)
+                BriarTheme(isDarkTheme = isDark) {
+                    when (screenState) {
+                        Screen.REGISTRATION ->
+                            Registration(registrationViewModel) {
+                                contactsViewModel.loadContacts()
+                                screenState = Screen.MAIN
+                            }
+                        Screen.LOGIN ->
+                            Login(loginViewModel) {
+                                contactsViewModel.loadContacts()
+                                screenState = Screen.MAIN
+                            }
+                        else ->
+                            CompositionLocalProvider(
+                                CVM provides conversationManager,
+                                CTM provides contactManager,
+                                MM provides messagingManager,
+                                IM provides identityManager,
+                            ) {
+                                BriarUIStateManager(contactsViewModel, isDark, setDark)
+                            }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index aaa11bb431b10935b96894e55c1e109fd4be7d76..7a55917cd28a774785de504291c8dd6dc4895ba8 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -48,7 +48,7 @@ internal class DesktopModule(private val appDir: Path) {
 
     @Provides
     @Singleton
-    internal fun provideBriarService(briarService: BriarServiceImpl): BriarService = briarService
+    internal fun provideBriarService(briarService: BriarUiImpl): BriarUi = briarService
 
     @Provides
     @Singleton
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
index 5a4e72ec8597505b4503779cfd44a47a0bafc3d4..1c401a2cfc3fbcdd11e814673272a4bd9b6ccacd 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
@@ -1,7 +1,6 @@
 package org.briarproject.briar.desktop
 
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.window.application
 import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.parameters.options.counted
 import com.github.ajalt.clikt.parameters.options.default
@@ -42,7 +41,7 @@ private class Main : CliktCommand(
     ).default(DEFAULT_DATA_DIR)
 
     @OptIn(ExperimentalComposeUiApi::class)
-    override fun run() = application {
+    override fun run() {
         val level = if (debug) ALL else when (verbosity) {
             0 -> WARNING
             1 -> INFO
@@ -61,7 +60,7 @@ private class Main : CliktCommand(
         BrambleCoreEagerSingletons.Helper.injectEagerSingletons(app)
         BriarCoreEagerSingletons.Helper.injectEagerSingletons(app)
 
-        app.getUI().startBriar()
+        app.getBriarUi().start()
     }
 
     private fun getDataDir(): Path {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/UI.kt b/src/main/kotlin/org/briarproject/briar/desktop/UI.kt
deleted file mode 100644
index f12d0c60dcd211072ecd736c187f2f3bd0dfba88..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/UI.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.briarproject.briar.desktop
-
-import androidx.compose.runtime.Composable
-import org.briarproject.bramble.api.account.AccountManager
-import org.briarproject.bramble.api.contact.ContactManager
-import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
-import org.briarproject.bramble.api.event.EventBus
-import org.briarproject.bramble.api.identity.IdentityManager
-import org.briarproject.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.introduction.IntroductionManager
-import org.briarproject.briar.api.messaging.MessagingManager
-import org.briarproject.briar.api.messaging.PrivateMessageFactory
-import java.util.logging.Logger.getLogger
-import javax.annotation.concurrent.Immutable
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Immutable
-@Singleton
-internal class UI
-@Inject
-constructor(
-    private val briarService: BriarService,
-    private val accountManager: AccountManager,
-    private val contactManager: ContactManager,
-    private val messagingManager: MessagingManager,
-    private val introductionManager: IntroductionManager,
-    private val conversationManager: ConversationManager,
-    private val identityManager: IdentityManager,
-    private val privateMessageFactory: PrivateMessageFactory,
-    private val eventBus: EventBus,
-    private val passwordStrengthEstimator: PasswordStrengthEstimator
-) {
-
-    private val logger = getLogger(UI::javaClass.name)
-
-    @Composable
-    internal fun startBriar() {
-        briarService.start(
-            contactManager,
-            conversationManager,
-            messagingManager,
-            identityManager
-        )
-    }
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..194155a15845287818e8c7197182447571fbbfa4
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt
@@ -0,0 +1,28 @@
+package org.briarproject.briar.desktop.dialogs
+
+import androidx.compose.runtime.mutableStateListOf
+import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.bramble.api.contact.ContactManager
+import java.util.logging.Logger
+import javax.inject.Inject
+
+class ContactsViewModel
+@Inject
+constructor(
+    private val contactManager: ContactManager,
+) {
+
+    companion object {
+        private val LOG = Logger.getLogger(ContactsViewModel::class.java.name)
+    }
+
+    internal val contacts = mutableStateListOf<Contact>()
+
+    internal fun loadContacts() {
+        val contacts = contactManager.contacts
+        for (contact in contacts) {
+            LOG.info("loaded contact: ${contact.author.name} (${contact.alias})")
+            this.contacts.add(contact)
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt
index 7be9e7caca821cedc3d161b3a9740e8ae6937ea7..a61f33837cd4a0e951f45f518c39992595e0eb5c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt
@@ -17,10 +17,7 @@ import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -42,10 +39,16 @@ import androidx.compose.ui.unit.dp
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun Login(
+    viewModel: LoginViewModel,
     modifier: Modifier = Modifier,
-    onResult: (result: String) -> Unit
+    onSignedIn: () -> Unit
 ) {
-    var password by remember { mutableStateOf("") }
+    val signIn = {
+        viewModel.signIn {
+            onSignedIn()
+        }
+    }
+
     val initialFocusRequester = remember { FocusRequester() }
     Surface {
         Column(
@@ -56,24 +59,24 @@ fun Login(
             BriarLogo()
             Spacer(Modifier.height(32.dp))
             OutlinedTextField(
-                value = password,
-                onValueChange = { password = it },
+                value = viewModel.password.value,
+                onValueChange = viewModel::setPassword,
                 label = { Text("Password") },
                 singleLine = true,
                 visualTransformation = PasswordVisualTransformation(),
                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
-                keyboardActions = KeyboardActions(onDone = { onResult.invoke(password) }),
+                keyboardActions = KeyboardActions(onDone = { signIn() }),
                 modifier = Modifier
                     .focusRequester(initialFocusRequester)
                     .onPreviewKeyEvent {
                         if (it.type == KeyEventType.KeyUp && it.key == Key.Enter) {
-                            onResult.invoke(password)
+                            signIn()
                         }
                         false
                     },
             )
             Spacer(Modifier.height(16.dp))
-            Button(onClick = { onResult.invoke(password) }) {
+            Button(onClick = { signIn() }) {
                 Text("Login")
             }
 
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..228a8012197763b092fd5c0e3fe213dcf0f382df
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt
@@ -0,0 +1,40 @@
+package org.briarproject.briar.desktop.dialogs
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.account.AccountManager
+import org.briarproject.bramble.api.crypto.DecryptionException
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import javax.inject.Inject
+
+class LoginViewModel
+@Inject
+constructor(
+    private val accountManager: AccountManager,
+    private val lifecycleManager: LifecycleManager,
+) {
+
+    private val _password = mutableStateOf("")
+
+    val password: State<String> = _password
+
+    fun setPassword(password: String) {
+        _password.value = password
+    }
+
+    fun signIn(success: () -> Unit) {
+        try {
+            accountManager.signIn(password.value)
+            signedIn()
+            success()
+        } catch (e: DecryptionException) {
+            // failure, try again
+        }
+    }
+
+    private fun signedIn() {
+        val dbKey = accountManager.databaseKey ?: throw AssertionError()
+        lifecycleManager.startServices(dbKey)
+        lifecycleManager.waitForStartup()
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt
index afab8ce46adc62a904ffdb9638ae118cd411ad41..7cc06944c6aa53ab3f8b69aad27363c6248750db 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt
@@ -10,13 +10,11 @@ import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.Button
 import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -40,61 +38,68 @@ import androidx.compose.ui.unit.dp
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun Registration(
+    viewModel: RegistrationViewModel,
     modifier: Modifier = Modifier,
-    onSubmit: (username: String, password: String) -> Unit
+    onSignedUp: () -> Unit
 ) {
-    var username by remember { mutableStateOf("") }
-    var password by remember { mutableStateOf("") }
+    val signUp = {
+        viewModel.signUp {
+            onSignedUp()
+        }
+    }
+
     val initialFocusRequester = remember { FocusRequester() }
     val focusManager = LocalFocusManager.current
-    Column(
-        modifier = modifier.padding(16.dp).fillMaxSize(),
-        verticalArrangement = Arrangement.Center,
-        horizontalAlignment = Alignment.CenterHorizontally
-    ) {
-        BriarLogo()
-        Spacer(Modifier.height(32.dp))
-        OutlinedTextField(
-            value = username,
-            onValueChange = { username = it },
-            label = { Text("Username") },
-            singleLine = true,
-            textStyle = TextStyle(color = Color.White),
-            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
-            keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
-            modifier = Modifier
-                .focusRequester(initialFocusRequester)
-                .onPreviewKeyEvent {
+    Surface {
+        Column(
+            modifier = modifier.padding(16.dp).fillMaxSize(),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            BriarLogo()
+            Spacer(Modifier.height(32.dp))
+            OutlinedTextField(
+                value = viewModel.username.value,
+                onValueChange = { viewModel.setUsername(it) },
+                label = { Text("Username") },
+                singleLine = true,
+                textStyle = TextStyle(color = Color.White),
+                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+                keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
+                modifier = Modifier
+                    .focusRequester(initialFocusRequester)
+                    .onPreviewKeyEvent {
+                        if (it.type == KeyEventType.KeyUp && it.key == Key.Enter) {
+                            focusManager.moveFocus(FocusDirection.Next)
+                        }
+                        false
+                    },
+            )
+            OutlinedTextField(
+                value = viewModel.password.value,
+                onValueChange = { viewModel.setPassword(it) },
+                label = { Text("Password") },
+                singleLine = true,
+                textStyle = TextStyle(color = Color.White),
+                visualTransformation = PasswordVisualTransformation(),
+                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
+                keyboardActions = KeyboardActions(onDone = { signUp() }),
+                modifier = Modifier.onPreviewKeyEvent {
                     if (it.type == KeyEventType.KeyUp && it.key == Key.Enter) {
-                        focusManager.moveFocus(FocusDirection.Next)
+                        signUp()
                     }
                     false
                 },
-        )
-        OutlinedTextField(
-            value = password,
-            onValueChange = { password = it },
-            label = { Text("Password") },
-            singleLine = true,
-            textStyle = TextStyle(color = Color.White),
-            visualTransformation = PasswordVisualTransformation(),
-            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
-            keyboardActions = KeyboardActions(onDone = { onSubmit.invoke(username, password) }),
-            modifier = Modifier.onPreviewKeyEvent {
-                if (it.type == KeyEventType.KeyUp && it.key == Key.Enter) {
-                    onSubmit.invoke(username, password)
-                }
-                false
-            },
-        )
-        Spacer(Modifier.height(16.dp))
-        Button(onClick = { onSubmit.invoke(username, password) }) {
-            Text("Register", color = Color.Black)
-        }
+            )
+            Spacer(Modifier.height(16.dp))
+            Button(onClick = { signUp() }) {
+                Text("Register", color = Color.Black)
+            }
 
-        DisposableEffect(Unit) {
-            initialFocusRequester.requestFocus()
-            onDispose { }
+            DisposableEffect(Unit) {
+                initialFocusRequester.requestFocus()
+                onDispose { }
+            }
         }
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b5a9b016e02a2ef18ee3b383285bc749e2ea60e6
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt
@@ -0,0 +1,46 @@
+package org.briarproject.briar.desktop.dialogs
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.account.AccountManager
+import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import javax.inject.Inject
+
+class RegistrationViewModel
+@Inject
+constructor(
+    private val accountManager: AccountManager,
+    private val lifecycleManager: LifecycleManager,
+    private val passwordStrengthEstimator: PasswordStrengthEstimator,
+) {
+
+    private var isSafeEnough = mutableStateOf(false)
+    private val _username = mutableStateOf("")
+    private val _password = mutableStateOf("")
+
+    val username: State<String> = _username
+    val password: State<String> = _password
+
+    fun setUsername(username: String) {
+        _username.value = username
+    }
+
+    fun setPassword(password: String) {
+        _password.value = password
+        // TODO: decide on useful value here
+        isSafeEnough.value = passwordStrengthEstimator.estimateStrength(password) > 0
+    }
+
+    fun signUp(success: () -> Unit) {
+        accountManager.createAccount(_username.value, _password.value)
+        signedIn()
+        success()
+    }
+
+    private fun signedIn() {
+        val dbKey = accountManager.databaseKey ?: throw AssertionError()
+        lifecycleManager.startServices(dbKey)
+        lifecycleManager.waitForStartup()
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt
index 3babe30d44caa4dd82254063e75c54cd64e9b5b1..d8699185fe69987793dbac0c4a821bb7c4babe4d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.briar.desktop.dialogs.ContactsViewModel
 
 enum class UiModes {
     CONTACTS,
@@ -31,7 +32,7 @@ enum class UiModes {
  */
 @Composable
 fun BriarUIStateManager(
-    contacts: List<Contact>,
+    contactsViewModel: ContactsViewModel,
     isDark: Boolean,
     setDark: (Boolean) -> Unit
 ) {
@@ -39,9 +40,9 @@ fun BriarUIStateManager(
     val (uiMode, setUiMode) = remember { mutableStateOf(UiModes.CONTACTS) }
     // TODO Figure out how to handle accounts with 0 contacts
     // current selected contact
-    val (contact, setContact) = remember { mutableStateOf(contact(contacts)) }
+    val (contact, setContact) = remember { mutableStateOf(contact(contactsViewModel)) }
     // current selected private group
-    val (group, setGroup) = remember { mutableStateOf(contact(contacts)) }
+    val (group, setGroup) = remember { mutableStateOf(contact(contactsViewModel)) }
     // current selected forum
     val (forum, setForum) = remember { mutableStateOf(0) }
     // current blog state
@@ -57,7 +58,7 @@ fun BriarUIStateManager(
         when (uiMode) {
             UiModes.CONTACTS -> if (contact != null) PrivateMessageView(
                 contact,
-                contacts,
+                contactsViewModel,
                 setContact
             )
             UiModes.SETTINGS -> PlaceHolderSettingsView(isDark, setDark)
@@ -79,6 +80,6 @@ fun PlaceHolderSettingsView(isDark: Boolean, setDark: (Boolean) -> Unit) {
     }
 }
 
-fun contact(contacts: List<Contact>): Contact? {
-    return if (contacts.isEmpty()) null else contacts[0]
+fun contact(contacts: ContactsViewModel): Contact? {
+    return if (contacts.contacts.isEmpty()) null else contacts.contacts[0]
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt
index fcadca087dd27e32090fa7b2949ab76514ac692f..a05d9cae64e0d171be6911dda7ef1e62d8443634 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt
@@ -88,6 +88,7 @@ import org.briarproject.briar.desktop.chat.ChatHistoryConversationVisitor
 import org.briarproject.briar.desktop.chat.ConversationMessageHeaderComparator
 import org.briarproject.briar.desktop.chat.SimpleMessage
 import org.briarproject.briar.desktop.chat.UiState
+import org.briarproject.briar.desktop.dialogs.ContactsViewModel
 import org.briarproject.briar.desktop.paul.theme.DarkColors
 import org.briarproject.briar.desktop.paul.theme.awayMsgBubble
 import org.briarproject.briar.desktop.paul.theme.divider
@@ -127,7 +128,7 @@ fun VerticalDivider() {
 @Composable
 fun PrivateMessageView(
     contact: Contact,
-    contacts: List<Contact>,
+    contacts: ContactsViewModel,
     onContactSelect: (Contact) -> Unit
 ) {
     val (isDialogVisible, setDialogVisibility) = remember { mutableStateOf(false) }
@@ -136,12 +137,12 @@ fun PrivateMessageView(
     val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
     AddContactDialog(isDialogVisible, setDialogVisibility)
     Row(modifier = Modifier.fillMaxWidth()) {
-        ContactList(contact, contacts, onContactSelect, setDialogVisibility)
+        ContactList(contact, contacts.contacts, onContactSelect, setDialogVisibility)
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
             Conversation(
                 contact,
-                contacts,
+                contacts.contacts,
                 dropdownExpanded,
                 setExpanded,
                 infoDrawer,
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt b/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt
index dd1f8ca9f4c442b752ae90be9583ff946672c5af..7265ea99041803a6139439011b07b5a10791fbfe 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/BriarDesktopTestApp.kt
@@ -23,7 +23,7 @@ import javax.inject.Singleton
 @Singleton
 internal interface BriarDesktopTestApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons {
 
-    fun getUI(): UI
+    fun getBriarUi(): BriarUi
 
     fun getSecureRandom(): SecureRandom
 
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index cebf66dfaaeb9251699e49a1da429448cf10c560..dde9e5483f3bc1714a68b7d623029bb923ff4164 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -52,7 +52,7 @@ internal class DesktopTestModule(private val appDir: File) {
 
     @Provides
     @Singleton
-    internal fun provideBriarService(briarService: BriarServiceImpl): BriarService = briarService
+    internal fun provideBriarService(briarService: BriarUiImpl): BriarUi = briarService
 
     @Provides
     @Singleton
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
index d7860028a8e2b074153bfd928e0e6b668e74c7db..e3375e8483d8f4f6e302b7cfc8c36f60a0dd0f49 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
@@ -56,7 +56,7 @@ internal class RunWithTemporaryAccount(val customization: BriarDesktopTestApp.()
         // list yet, we need to wait a moment in order for that to finish (hopefully).
         Thread.sleep(1000)
 
-        app.getUI().startBriar()
+        app.getBriarUi().start()
     }
 
     private fun getDataDir(): Path {