diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
index a76138c1f98b83e0230ae782e2ab7f4a8012380f..123ce4396f9b0d1e08a7c888792e5bca4cd304f7 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
@@ -43,7 +43,7 @@ public interface DatabaseComponent {
 	 * @throws DataTooOldException if the data uses an older schema than the
 	 * current code and cannot be migrated
 	 */
-	boolean open() throws DbException;
+	boolean open(@Nullable MigrationListener listener) throws DbException;
 
 	/**
 	 * Waits for any open transactions to finish and closes the database.
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseMigrationEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseMigrationEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..7edd81fa0c19a7b4b8126d591141376e85f8355b
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseMigrationEvent.java
@@ -0,0 +1,9 @@
+package org.briarproject.bramble.api.db;
+
+import org.briarproject.bramble.api.event.Event;
+
+/**
+ * An event that is broadcast before database migrations are being applied.
+ */
+public class DatabaseMigrationEvent extends Event {
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..79e292a75f3b9afd9cb40b1c95911afbaa4223dd
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java
@@ -0,0 +1,11 @@
+package org.briarproject.bramble.api.db;
+
+public interface MigrationListener {
+
+	/**
+	 * This is called when a migration is started while opening the database.
+	 * It will be called once for each migration being applied.
+	 */
+	void onMigrationRun();
+
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
index b70c9dec9b2a624e88d8d5dd9c7519810f9b08fb..95b4ac08c19a5724c3da5e1e4277bae6e8c70b6f 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
@@ -29,6 +29,14 @@ public interface LifecycleManager {
 		SUCCESS
 	}
 
+	/**
+	 * The state the lifecycle can be in.
+	 * Returned by {@link #getLifecycleState()}
+	 */
+	enum LifecycleState {
+		STARTING, MIGRATING, RUNNING
+	}
+
 	/**
 	 * Registers a {@link Service} to be started and stopped.
 	 */
@@ -76,4 +84,10 @@ public interface LifecycleManager {
 	 * the {@link DatabaseComponent} to be closed before returning.
 	 */
 	void waitForShutdown() throws InterruptedException;
+
+	/**
+	 * Returns the current state of the lifecycle.
+	 */
+	LifecycleState getLifecycleState();
+
 }
\ No newline at end of file
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/event/StartupEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/event/StartupEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..973bd1c4700bfcaf6f4fa78eea5dd1aca36071cd
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/event/StartupEvent.java
@@ -0,0 +1,10 @@
+package org.briarproject.bramble.api.lifecycle.event;
+
+import org.briarproject.bramble.api.event.Event;
+
+/**
+ * An event that is broadcast when the app is starting.
+ * This happens after the database was opened and services were started.
+ */
+public class StartupEvent extends Event {
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index 9d1c7636498a869c856ad57a134514725fdd2dbc..0a6e54fbd85d47b5417444ab5b0c2b0513004289 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.DataTooNewException;
 import org.briarproject.bramble.api.db.DataTooOldException;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.LocalAuthor;
@@ -45,7 +46,7 @@ interface Database<T> {
 	 * @throws DataTooOldException if the data uses an older schema than the
 	 * current code and cannot be migrated
 	 */
-	boolean open() throws DbException;
+	boolean open(@Nullable MigrationListener listener) throws DbException;
 
 	/**
 	 * Prevents new transactions from starting, waits for all current
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index af80da92b3c7f8d7499b1a0e126d0af9f75f42e0..5809c09eef279fb8b284c31bfb5b049027d28a57 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.db.ContactExistsException;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.db.NoSuchContactException;
 import org.briarproject.bramble.api.db.NoSuchGroupException;
 import org.briarproject.bramble.api.db.NoSuchLocalAuthorException;
@@ -100,8 +101,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	}
 
 	@Override
-	public boolean open() throws DbException {
-		boolean reopened = db.open();
+	public boolean open(@Nullable MigrationListener listener)
+			throws DbException {
+		boolean reopened = db.open(listener);
 		shutdown.addShutdownHook(() -> {
 			try {
 				close();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
index 6a81969260df8283a07c2ee9ab6a48efdd040778..1c1983e6771a2cc174ebcbf68ede5879aab7569f 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.db.DatabaseConfig;
 import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.util.StringUtils;
@@ -13,6 +14,7 @@ import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.util.Properties;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 /**
@@ -42,10 +44,11 @@ class H2Database extends JdbcDatabase {
 	}
 
 	@Override
-	public boolean open() throws DbException {
+	public boolean open(@Nullable MigrationListener listener)
+			throws DbException {
 		boolean reopen = config.databaseExists();
 		if (!reopen) config.getDatabaseDirectory().mkdirs();
-		super.open("org.h2.Driver", reopen);
+		super.open("org.h2.Driver", reopen, listener);
 		return reopen;
 	}
 
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
index 1db9248cb28b6d67f7de3818b822480296dabf5e..6a87ededae7b862c992745f15ac5695f881a6cf0 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.db.DatabaseConfig;
 import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.util.StringUtils;
@@ -13,6 +14,7 @@ import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 /**
@@ -44,10 +46,10 @@ class HyperSqlDatabase extends JdbcDatabase {
 	}
 
 	@Override
-	public boolean open() throws DbException {
+	public boolean open(@Nullable MigrationListener listener) throws DbException {
 		boolean reopen = config.databaseExists();
 		if (!reopen) config.getDatabaseDirectory().mkdirs();
-		super.open("org.hsqldb.jdbc.JDBCDriver", reopen);
+		super.open("org.hsqldb.jdbc.JDBCDriver", reopen, listener);
 		return reopen;
 	}
 
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index f1f795a8c1eb85cb33dd73c7894ad144a9649fde..e71f30c587215dda0cd8510981258e5fdc94c548 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.db.DataTooOldException;
 import org.briarproject.bramble.api.db.DbClosedException;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.LocalAuthor;
@@ -301,7 +302,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		this.clock = clock;
 	}
 
-	protected void open(String driverClass, boolean reopen) throws DbException {
+	protected void open(String driverClass, boolean reopen,
+			@Nullable MigrationListener listener) throws DbException {
 		// Load the JDBC driver
 		try {
 			Class.forName(driverClass);
@@ -312,7 +314,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		Connection txn = startTransaction();
 		try {
 			if (reopen) {
-				checkSchemaVersion(txn);
+				checkSchemaVersion(txn, listener);
 			} else {
 				createTables(txn);
 				storeSchemaVersion(txn, CODE_SCHEMA_VERSION);
@@ -335,7 +337,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 	 * @throws DataTooOldException if the data uses an older schema than the
 	 * current code and cannot be migrated
 	 */
-	private void checkSchemaVersion(Connection txn) throws DbException {
+	private void checkSchemaVersion(Connection txn,
+			@Nullable MigrationListener listener) throws DbException {
 		Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
 		int dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
 		if (dataSchemaVersion == -1) throw new DbException();
@@ -348,6 +351,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (start == dataSchemaVersion) {
 				if (LOG.isLoggable(INFO))
 					LOG.info("Migrating from schema " + start + " to " + end);
+				if (listener != null) listener.onMigrationRun();
 				// Apply the migration
 				m.migrate(txn);
 				// Store the new schema version
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
index 029170c911c4814c76b020115c6172a47ef12f79..6e55a8df58f3589211ee519bad9d6e6dd025dde9 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
@@ -5,7 +5,9 @@ import org.briarproject.bramble.api.crypto.KeyPair;
 import org.briarproject.bramble.api.db.DataTooNewException;
 import org.briarproject.bramble.api.db.DataTooOldException;
 import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DatabaseMigrationEvent;
 import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.MigrationListener;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.identity.AuthorFactory;
@@ -15,6 +17,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.lifecycle.Service;
 import org.briarproject.bramble.api.lifecycle.ServiceException;
 import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
+import org.briarproject.bramble.api.lifecycle.event.StartupEvent;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 
@@ -31,6 +34,9 @@ import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING;
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR;
@@ -40,7 +46,7 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResul
 
 @ThreadSafe
 @NotNullByDefault
-class LifecycleManagerImpl implements LifecycleManager {
+class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(LifecycleManagerImpl.class.getName());
@@ -57,6 +63,7 @@ class LifecycleManagerImpl implements LifecycleManager {
 	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, EventBus eventBus,
@@ -123,7 +130,7 @@ class LifecycleManagerImpl implements LifecycleManager {
 			LOG.info("Starting services");
 			long start = System.currentTimeMillis();
 
-			boolean reopened = db.open();
+			boolean reopened = db.open(this);
 			long duration = System.currentTimeMillis() - start;
 			if (LOG.isLoggable(INFO)) {
 				if (reopened)
@@ -162,6 +169,8 @@ class LifecycleManagerImpl implements LifecycleManager {
 				}
 			}
 			startupLatch.countDown();
+			state = RUNNING;
+			eventBus.broadcast(new StartupEvent());
 			return SUCCESS;
 		} catch (DataTooOldException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -180,6 +189,12 @@ class LifecycleManagerImpl implements LifecycleManager {
 		}
 	}
 
+	@Override
+	public void onMigrationRun() {
+		state = MIGRATING;
+		eventBus.broadcast(new DatabaseMigrationEvent());
+	}
+
 	@Override
 	public void stopServices() {
 		try {
@@ -235,4 +250,8 @@ class LifecycleManagerImpl implements LifecycleManager {
 		shutdownLatch.await();
 	}
 
+	@Override
+	public LifecycleState getLifecycleState() {
+		return state;
+	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index b2e4842db87c3f0a6633f1196756be456b7671b9..18de0d7e847f57156dd7ebbcab6474bd63e21404 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -134,7 +134,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		int shutdownHandle = 12345;
 		context.checking(new Expectations() {{
 			// open()
-			oneOf(database).open();
+			oneOf(database).open(null);
 			will(returnValue(false));
 			oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
 			will(returnValue(shutdownHandle));
@@ -201,7 +201,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		Transaction transaction = db.startTransaction(false);
 		try {
 			db.addLocalAuthor(transaction, localAuthor);
@@ -1501,7 +1501,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
 		context.checking(new Expectations() {{
 			// open()
-			oneOf(database).open();
+			oneOf(database).open(null);
 			will(returnValue(false));
 			oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
 			will(returnValue(shutdownHandle));
@@ -1543,7 +1543,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		Transaction transaction = db.startTransaction(false);
 		try {
 			db.addLocalMessage(transaction, message, metadata, true);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseMigrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseMigrationTest.java
index 203f4dfd21f6fd2b09ceed436f3a10dfca9696b7..89bdf8a4c1a8da94963bc41c7473be59b3f7c063 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseMigrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseMigrationTest.java
@@ -62,7 +62,7 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 	public void testDoesNotRunMigrationsWhenCreatingDatabase()
 			throws Exception {
 		Database<Connection> db = createDatabase(singletonList(migration));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		db.close();
 	}
@@ -72,14 +72,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 			throws Exception {
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		// Override the data schema version
 		setDataSchemaVersion(db, -1);
 		db.close();
 		// Reopen the DB - an exception should be thrown
 		db = createDatabase(asList(migration, migration1));
-		db.open();
+		db.open(null);
 	}
 
 	@Test
@@ -87,12 +87,12 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 			throws Exception {
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		db.close();
 		// Reopen the DB - migrations should not be run
 		db = createDatabase(asList(migration, migration1));
-		assertTrue(db.open());
+		assertTrue(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		db.close();
 	}
@@ -101,14 +101,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 	public void testThrowsExceptionIfDataIsNewerThanCode() throws Exception {
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		// Override the data schema version
 		setDataSchemaVersion(db, CODE_SCHEMA_VERSION + 1);
 		db.close();
 		// Reopen the DB - an exception should be thrown
 		db = createDatabase(asList(migration, migration1));
-		db.open();
+		db.open(null);
 	}
 
 	@Test(expected = DataTooOldException.class)
@@ -116,13 +116,13 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 			throws Exception {
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(emptyList());
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 1);
 		db.close();
 		// Reopen the DB - an exception should be thrown
 		db = createDatabase(emptyList());
-		db.open();
+		db.open(null);
 	}
 
 	@Test(expected = DataTooOldException.class)
@@ -141,14 +141,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		// Override the data schema version
 		setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 3);
 		db.close();
 		// Reopen the DB - an exception should be thrown
 		db = createDatabase(asList(migration, migration1));
-		db.open();
+		db.open(null);
 	}
 
 	@Test
@@ -170,14 +170,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		// Override the data schema version
 		setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
 		db.close();
 		// Reopen the DB - the first migration should be run
 		db = createDatabase(asList(migration, migration1));
-		assertTrue(db.open());
+		assertTrue(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		db.close();
 	}
@@ -202,14 +202,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
 
 		// Open the DB for the first time
 		Database<Connection> db = createDatabase(asList(migration, migration1));
-		assertFalse(db.open());
+		assertFalse(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		// Override the data schema version
 		setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
 		db.close();
 		// Reopen the DB - both migrations should be run
 		db = createDatabase(asList(migration, migration1));
-		assertTrue(db.open());
+		assertTrue(db.open(null));
 		assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
 		db.close();
 	}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceComparisonTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceComparisonTest.java
index c2a124bb299c1729be0e9c83da3ad7555c65b82c..1807d30dc33374654a740b4acb58336bee1f5b52 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceComparisonTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceComparisonTest.java
@@ -71,7 +71,7 @@ public abstract class DatabasePerformanceComparisonTest
 			throws DbException {
 		Database<Connection> db = createDatabase(conditionA,
 				new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock());
-		db.open();
+		db.open(null);
 		return db;
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseTraceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseTraceTest.java
index a78fb5f55a4aa93fd3929bd6e583c5e6a676cf6c..f4e0daa19dedf3d6e0187a4aa1b7e8d181136906 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseTraceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseTraceTest.java
@@ -43,7 +43,7 @@ public abstract class DatabaseTraceTest extends DatabasePerformanceTest {
 	private Database<Connection> openDatabase() throws DbException {
 		Database<Connection> db = createDatabase(
 				new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock());
-		db.open();
+		db.open(null);
 		return db;
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index 32d4e5faa4b36b6330f0eeb1c2ab404dde264d7d..81d1c902578e1eaa6f7b6e3ba84fda69ad9a5b0a 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -1700,7 +1700,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		Database<Connection> db = createDatabase(
 				new TestDatabaseConfig(testDir, MAX_SIZE), clock);
 		if (!resume) TestUtils.deleteTestDirectory(testDir);
-		db.open();
+		db.open(null);
 		return db;
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/SingleDatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/SingleDatabasePerformanceTest.java
index 5a836476782da918dc9047721839c60402f5a76b..670c7cddb79746be691317387a0c779d0c6b5213 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/SingleDatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/SingleDatabasePerformanceTest.java
@@ -40,7 +40,7 @@ public abstract class SingleDatabasePerformanceTest
 	private Database<Connection> openDatabase() throws DbException {
 		Database<Connection> db = createDatabase(
 				new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock());
-		db.open();
+		db.open(null);
 		return db;
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestLifecycleModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestLifecycleModule.java
index ee8d5115e07b3285bb0ac5be2251878102d84935..15943adc2b6dabfa86fc8ebb72d270ad396a00ed 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestLifecycleModule.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestLifecycleModule.java
@@ -17,6 +17,8 @@ import javax.inject.Singleton;
 import dagger.Module;
 import dagger.Provides;
 
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
+
 @Module
 public class TestLifecycleModule {
 
@@ -57,6 +59,11 @@ public class TestLifecycleModule {
 			@Override
 			public void waitForShutdown() throws InterruptedException {
 			}
+
+			@Override
+			public LifecycleState getLifecycleState() {
+				return RUNNING;
+			}
 		};
 		return lifecycleManager;
 	}
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index a2fed381f9a296c31c69b8e918ab4d7658b49c2e..102de1290c936f258fbc59e2c1714eda23fe5320 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -77,6 +77,11 @@
 			</intent-filter>
 		</activity>
 
+		<activity
+			android:name=".android.login.OpenDatabaseActivity"
+			android:label="@string/app_name"
+			android:launchMode="singleTop"/>
+
 		<activity
 			android:name="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
 			android:theme="@style/BriarTheme.NoActionBar"
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
index abd9a20a7977c2ff452fbdcb4f1cb86bb5e9014b..2c618cf924eb55bf61fd06b0d0b9295a30a1e445 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
 import org.briarproject.bramble.api.system.AndroidExecutor;
 import org.briarproject.briar.R;
+import org.briarproject.briar.android.login.OpenDatabaseActivity;
 import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
 
 import java.util.concurrent.CountDownLatch;
@@ -88,6 +89,12 @@ public class BriarService extends Service {
 			stopSelf();
 			return;
 		}
+
+		Intent intent =
+				new Intent(getApplicationContext(), OpenDatabaseActivity.class);
+		intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
+		startActivity(intent);
+
 		// Create notification channels
 		if (SDK_INT >= 26) {
 			NotificationManager nm = (NotificationManager)
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
index 7403d01e019fa91893cb2b59ea45faf243131f5c..2f8ca83faa6bd97999870c692cf6452479a23f70 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
@@ -32,6 +32,7 @@ import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
 import org.briarproject.briar.android.login.AuthorNameFragment;
 import org.briarproject.briar.android.login.ChangePasswordActivity;
 import org.briarproject.briar.android.login.DozeFragment;
+import org.briarproject.briar.android.login.OpenDatabaseActivity;
 import org.briarproject.briar.android.login.PasswordActivity;
 import org.briarproject.briar.android.login.PasswordFragment;
 import org.briarproject.briar.android.login.SetupActivity;
@@ -88,6 +89,8 @@ public interface ActivityComponent {
 
 	void inject(SetupActivity activity);
 
+	void inject(OpenDatabaseActivity activity);
+
 	void inject(NavDrawerActivity activity);
 
 	void inject(PasswordActivity activity);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..f197f13ec0a6c100dd74b6ef905fcfd08a09c145
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java
@@ -0,0 +1,90 @@
+package org.briarproject.briar.android.login;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.briarproject.bramble.api.db.DatabaseMigrationEvent;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.lifecycle.event.StartupEvent;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.activity.ActivityComponent;
+import org.briarproject.briar.android.activity.BaseActivity;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING;
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
+
+@ParametersAreNonnullByDefault
+public class OpenDatabaseActivity extends BaseActivity
+		implements EventListener {
+
+	@Inject
+	LifecycleManager lifecycleManager;
+	@Inject
+	EventBus eventBus;
+
+	private TextView textView;
+	private ImageView imageView;
+	private boolean showingMigration = false;
+
+	@Override
+	public void onCreate(@Nullable Bundle state) {
+		super.onCreate(state);
+		setContentView(R.layout.activity_open_database);
+		textView = findViewById(R.id.textView);
+		imageView = findViewById(R.id.imageView);
+	}
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public void onBackPressed() {
+		// do not let the user bail out of here
+	}
+
+	@Override
+	protected void onStart() {
+		super.onStart();
+		if (lifecycleManager.getLifecycleState() == RUNNING) {
+			supportFinishAfterTransition();
+		} else {
+			if (lifecycleManager.getLifecycleState() == MIGRATING) {
+				showMigration();
+			}
+			eventBus.addListener(this);
+		}
+	}
+
+	@Override
+	protected void onStop() {
+		super.onStop();
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof StartupEvent) {
+			runOnUiThreadUnlessDestroyed(this::supportFinishAfterTransition);
+		} else if (e instanceof DatabaseMigrationEvent) {
+			runOnUiThreadUnlessDestroyed(this::showMigration);
+		}
+	}
+
+	private void showMigration() {
+		if (showingMigration) return;
+		textView.setText(R.string.startup_migrate_database);
+		imageView.setImageResource(R.drawable.startup_migration);
+		showingMigration = true;
+	}
+
+}
diff --git a/briar-android/src/main/res/drawable/startup_lock.xml b/briar-android/src/main/res/drawable/startup_lock.xml
new file mode 100644
index 0000000000000000000000000000000000000000..181a64cc9162141b4bfe208b94a70bf94d4b31da
--- /dev/null
+++ b/briar-android/src/main/res/drawable/startup_lock.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="64dp"
+        android:height="64dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
+</vector>
diff --git a/briar-android/src/main/res/drawable/startup_migration.xml b/briar-android/src/main/res/drawable/startup_migration.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a5021ef0dad493614ddb3e068540d5cf992d6e77
--- /dev/null
+++ b/briar-android/src/main/res/drawable/startup_migration.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="64dp"
+        android:height="64dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1 -2.73,2.71 -2.73,7.08 0,9.79 2.73,2.71 7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29 -3.51,3.48 -9.21,3.48 -12.72,0 -3.5,-3.47 -3.53,-9.11 -0.02,-12.58 3.51,-3.47 9.14,-3.47 12.65,0L21,3v7.12zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z"/>
+</vector>
diff --git a/briar-android/src/main/res/layout/activity_open_database.xml b/briar-android/src/main/res/layout/activity_open_database.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ab20f17cb9dbb3a0924a9513f69f67a9f12e8b85
--- /dev/null
+++ b/briar-android/src/main/res/layout/activity_open_database.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<ImageView
+		android:id="@+id/imageView"
+		android:layout_width="128dp"
+		android:layout_height="128dp"
+		android:scaleType="center"
+		android:src="@drawable/startup_lock"
+		android:tint="@color/briar_primary"
+		app:layout_constraintBottom_toTopOf="@+id/textView"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent"
+		app:layout_constraintVertical_bias="0.5"
+		app:layout_constraintVertical_chainStyle="packed"
+		tools:ignore="ContentDescription"/>
+
+	<ProgressBar
+		android:id="@+id/progressBar"
+		style="?android:attr/progressBarStyleLarge"
+		android:layout_width="0dp"
+		android:layout_height="0dp"
+		app:layout_constraintBottom_toBottomOf="@+id/imageView"
+		app:layout_constraintEnd_toEndOf="@+id/imageView"
+		app:layout_constraintStart_toStartOf="@+id/imageView"
+		app:layout_constraintTop_toTopOf="@+id/imageView"/>
+
+	<TextView
+		android:id="@+id/textView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_margin="8dp"
+		android:text="@string/startup_open_database"
+		android:textSize="@dimen/text_size_large"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/imageView"/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index c682069f77c29abed6b07cb0a20e4b460e86ce90..14de9071b5dd7e220b2ed96e66999f29c6e197b2 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -46,6 +46,8 @@
 	</plurals>
 	<string name="expiry_update">The testing expiry date has been extended. Your account will now expire in %d days.</string>
 	<string name="expiry_date_reached">This software has expired.\nThank you for testing!</string>
+	<string name="startup_open_database">Decrypting Database…</string>
+	<string name="startup_migrate_database">Upgrading Database…</string>
 
 	<!-- Navigation Drawer -->
 	<string name="nav_drawer_open_description">Open the navigation drawer</string>