diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupFactory.java
index 6b4ba6b4c23d022475c6f436050316c48ca996c7..896acb221fe9bb07d7b8f69a07172bdb4bff4dbd 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupFactory.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupFactory.java
@@ -12,19 +12,19 @@ public interface ContactGroupFactory {
 	/**
 	 * Creates a group that is not shared with any contacts.
 	 */
-	Group createLocalGroup(ClientId clientId, int clientVersion);
+	Group createLocalGroup(ClientId clientId, int majorVersion);
 
 	/**
 	 * Creates a group for the given client to share with the given contact.
 	 */
-	Group createContactGroup(ClientId clientId, int clientVersion,
+	Group createContactGroup(ClientId clientId, int majorVersion,
 			Contact contact);
 
 	/**
 	 * Creates a group for the given client to share between the given authors
 	 * identified by their AuthorIds.
 	 */
-	Group createContactGroup(ClientId clientId, int clientVersion,
+	Group createContactGroup(ClientId clientId, int majorVersion,
 			AuthorId authorId1, AuthorId authorId2);
 
 }
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 ea05938d49c5a24fe0d0da14bd40d718e2605824..3715674413fe008428753cbad66bcd4f2601c3cd 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
@@ -241,7 +241,8 @@ public interface DatabaseComponent {
 	 * <p/>
 	 * Read-only.
 	 */
-	Collection<Group> getGroups(Transaction txn, ClientId c) throws DbException;
+	Collection<Group> getGroups(Transaction txn, ClientId c, int majorVersion)
+			throws DbException;
 
 	/**
 	 * Returns the given group's visibility to the given contact, or
@@ -266,6 +267,14 @@ public interface DatabaseComponent {
 	 */
 	Collection<LocalAuthor> getLocalAuthors(Transaction txn) throws DbException;
 
+	/**
+	 * Returns the IDs of all messages in the given group.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
+		throws DbException;
+
 	/**
 	 * Returns the IDs of any messages that need to be validated.
 	 * <p/>
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 5a036a3b6cc2005a36c28bbaacb67638256941a1..d3cd1bc245f33b855d522f47b32f0501689668b7 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
@@ -43,17 +43,20 @@ public interface LifecycleManager {
 	}
 
 	/**
-	 * Registers a {@link Service} to be started and stopped.
+	 * Registers a {@link Service} to be started and stopped. This method
+	 * should be called before {@link #startServices(String)}.
 	 */
 	void registerService(Service s);
 
 	/**
-	 * Registers a {@link Client} to be started.
+	 * Registers a {@link Client} to be started. This method should be called
+	 * before {@link #startServices(String)}.
 	 */
 	void registerClient(Client c);
 
 	/**
-	 * Registers an {@link ExecutorService} to be shut down.
+	 * Registers an {@link ExecutorService} to be shut down. This method
+	 * should be called before {@link #startServices(String)}.
 	 */
 	void registerForShutdown(ExecutorService e);
 
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
index fe33328a358b9feda1c21fe4eda9f9411cb8e791..a634bfaa74abac3057cdb548e01c4f8dee5e85d3 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/properties/TransportPropertyManager.java
@@ -15,12 +15,17 @@ public interface TransportPropertyManager {
 	/**
 	 * The unique ID of the transport property client.
 	 */
-	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.properties");
+	ClientId CLIENT_ID = new ClientId("org.briarproject.bramble.properties");
 
 	/**
-	 * The current version of the transport property client.
+	 * The current major version of the transport property client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the transport property client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Stores the given properties received while adding a contact - they will
@@ -37,8 +42,8 @@ public interface TransportPropertyManager {
 
 	/**
 	 * Returns the local transport properties for all transports.
-	 * <br/>
-	 * TODO: Transaction can be read-only when code is simplified
+	 * <p/>
+	 * Read-only.
 	 */
 	Map<TransportId, TransportProperties> getLocalProperties(Transaction txn)
 			throws DbException;
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/Group.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/Group.java
index 5ffe422c2ac7dd369a57ff3673335ddfb88eb25a..2d395cce55e59a5703423f0c43c3540c7f91dc80 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/Group.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/Group.java
@@ -5,9 +5,24 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPT
 public class Group {
 
 	public enum Visibility {
-		INVISIBLE, // The group is not visible
-		VISIBLE, // The group is visible but messages are not shared
-		SHARED // The group is visible and messages are shared
+
+		INVISIBLE(0), // The group is not visible
+		VISIBLE(1), // The group is visible, messages are accepted but not sent
+		SHARED(2); // The group is visible, messages are accepted and sent
+
+		private final int value;
+
+		Visibility(int value) {
+			this.value = value;
+		}
+
+		public int getValue() {
+			return value;
+		}
+
+		public static Visibility min(Visibility a, Visibility b) {
+			return a.getValue() < b.getValue() ? a : b;
+		}
 	}
 
 	/**
@@ -17,13 +32,16 @@ public class Group {
 
 	private final GroupId id;
 	private final ClientId clientId;
+	private final int majorVersion;
 	private final byte[] descriptor;
 
-	public Group(GroupId id, ClientId clientId, byte[] descriptor) {
+	public Group(GroupId id, ClientId clientId, int majorVersion,
+			byte[] descriptor) {
 		if (descriptor.length > MAX_GROUP_DESCRIPTOR_LENGTH)
 			throw new IllegalArgumentException();
 		this.id = id;
 		this.clientId = clientId;
+		this.majorVersion = majorVersion;
 		this.descriptor = descriptor;
 	}
 
@@ -41,6 +59,13 @@ public class Group {
 		return clientId;
 	}
 
+	/**
+	 * Returns the major version of the client to which the group belongs.
+	 */
+	public int getMajorVersion() {
+		return majorVersion;
+	}
+
 	/**
 	 * Returns the group's descriptor.
 	 */
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupFactory.java
index 844ded0e14863172165246aa13d534433c91df87..9924c51b8507a861e9439bdf625ed69bad785618 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupFactory.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupFactory.java
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 public interface GroupFactory {
 
 	/**
-	 * Creates a group with the given client ID, client version and descriptor.
+	 * Creates a group with the given client ID, major version and descriptor.
 	 */
-	Group createGroup(ClientId c, int clientVersion, byte[] descriptor);
+	Group createGroup(ClientId c, int majorVersion, byte[] descriptor);
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
index 80f48da6661ad3d924a0f85e7901a169b4b0e16e..5f196f147060a08fee698f2e4747c6f0609dc074 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
@@ -19,7 +19,9 @@ public interface SyncConstants {
 	 */
 	int MAX_RECORD_PAYLOAD_LENGTH = 48 * 1024; // 48 KiB
 
-	/** The maximum length of a group descriptor in bytes. */
+	/**
+	 * The maximum length of a group descriptor in bytes.
+	 */
 	int MAX_GROUP_DESCRIPTOR_LENGTH = 16 * 1024; // 16 KiB
 
 	/**
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/ValidationManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/ValidationManager.java
index d80042b41521aada66365f1bd21cdb1c620c114b..1718cac8101fc0b9a03719219b5bdaad23f72d84 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/ValidationManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/ValidationManager.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.sync;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Metadata;
 import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 
 /**
@@ -33,15 +34,20 @@ public interface ValidationManager {
 	}
 
 	/**
-	 * Sets the message validator for the given client.
+	 * Registers the message validator for the given client. This method
+	 * should be called before {@link LifecycleManager#startServices(String)}.
 	 */
-	void registerMessageValidator(ClientId c, MessageValidator v);
+	void registerMessageValidator(ClientId c, int majorVersion,
+			MessageValidator v);
 
 	/**
-	 * Sets the incoming message hook for the given client. The hook will be
-	 * called once for each incoming message that passes validation.
+	 * Registers the incoming message hook for the given client. The hook will
+	 * be called once for each incoming message that passes validation. This
+	 * method should be called before
+	 * {@link LifecycleManager#startServices(String)}.
 	 */
-	void registerIncomingMessageHook(ClientId c, IncomingMessageHook hook);
+	void registerIncomingMessageHook(ClientId c, int majorVersion,
+			IncomingMessageHook hook);
 
 	interface MessageValidator {
 
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientMajorVersion.java b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientMajorVersion.java
new file mode 100644
index 0000000000000000000000000000000000000000..d57eb987ebae611a494828be80a4a1fb57044eb0
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientMajorVersion.java
@@ -0,0 +1,50 @@
+package org.briarproject.bramble.api.versioning;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientId;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class ClientMajorVersion implements Comparable<ClientMajorVersion> {
+
+	private final ClientId clientId;
+	private final int majorVersion;
+
+	public ClientMajorVersion(ClientId clientId, int majorVersion) {
+		this.clientId = clientId;
+		this.majorVersion = majorVersion;
+	}
+
+	public ClientId getClientId() {
+		return clientId;
+	}
+
+	public int getMajorVersion() {
+		return majorVersion;
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (o instanceof ClientMajorVersion) {
+			ClientMajorVersion cv = (ClientMajorVersion) o;
+			return clientId.equals(cv.clientId)
+					&& majorVersion == cv.majorVersion;
+		}
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return (clientId.hashCode() << 16) + majorVersion;
+	}
+
+	@Override
+	public int compareTo(ClientMajorVersion cv) {
+		int compare = clientId.compareTo(cv.clientId);
+		if (compare != 0) return compare;
+		return majorVersion - cv.majorVersion;
+	}
+}
+
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..0cb2fc478d34206fa5b3fa3420d2c0486b747a42
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java
@@ -0,0 +1,45 @@
+package org.briarproject.bramble.api.versioning;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group.Visibility;
+
+@NotNullByDefault
+public interface ClientVersioningManager {
+
+	/**
+	 * The unique ID of the versioning client.
+	 */
+	ClientId CLIENT_ID = new ClientId("org.briarproject.bramble.versioning");
+
+	/**
+	 * The current major version of the versioning client.
+	 */
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * Registers a client that will be advertised to contacts. The hook will
+	 * be called when the visibility of the client changes. This method should
+	 * be called before {@link LifecycleManager#startServices(String)}.
+	 */
+	void registerClient(ClientId clientId, int majorVersion, int minorVersion,
+			ClientVersioningHook hook);
+
+	/**
+	 * Returns the visibility of the given client with respect to the given
+	 * contact.
+	 */
+	Visibility getClientVisibility(Transaction txn, ContactId contactId,
+			ClientId clientId, int majorVersion) throws DbException;
+
+	interface ClientVersioningHook {
+
+		void onClientVisibilityChanging(Transaction txn, Contact c,
+				Visibility v) throws DbException;
+	}
+}
diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
index 3ce8d154996355b2a96a6703fc854318c8a9d951..c77fde6e9921f5b5c337a29ef3873ce2b96c7456 100644
--- a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
+++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
@@ -117,15 +117,16 @@ public class TestUtils {
 		return new Author(id, FORMAT_VERSION, name, publicKey);
 	}
 
-	public static Group getGroup(ClientId clientId) {
+	public static Group getGroup(ClientId clientId, int majorVersion) {
 		int descriptorLength = 1 + random.nextInt(MAX_GROUP_DESCRIPTOR_LENGTH);
-		return getGroup(clientId, descriptorLength);
+		return getGroup(clientId, majorVersion, descriptorLength);
 	}
 
-	public static Group getGroup(ClientId clientId, int descriptorLength) {
+	public static Group getGroup(ClientId clientId, int majorVersion,
+			int descriptorLength) {
 		GroupId groupId = new GroupId(getRandomId());
 		byte[] descriptor = getRandomBytes(descriptorLength);
-		return new Group(groupId, clientId, descriptor);
+		return new Group(groupId, clientId, majorVersion, descriptor);
 	}
 
 	public static Message getMessage(GroupId groupId) {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
index e7a211e294761124ebb5313dfa2adee1bf9c3381..367e7785f06f264896d024a89b9798a5b9b6bd16 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
@@ -1,7 +1,7 @@
 package org.briarproject.bramble;
 
 import org.briarproject.bramble.contact.ContactModule;
-import org.briarproject.bramble.crypto.CryptoModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.db.DatabaseExecutorModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
@@ -10,12 +10,13 @@ import org.briarproject.bramble.properties.PropertiesModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 
 public interface BrambleCoreEagerSingletons {
 
 	void inject(ContactModule.EagerSingletons init);
 
-	void inject(CryptoModule.EagerSingletons init);
+	void inject(CryptoExecutorModule.EagerSingletons init);
 
 	void inject(DatabaseExecutorModule.EagerSingletons init);
 
@@ -32,4 +33,6 @@ public interface BrambleCoreEagerSingletons {
 	void inject(SystemModule.EagerSingletons init);
 
 	void inject(TransportModule.EagerSingletons init);
+
+	void inject(VersioningModule.EagerSingletons init);
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
index 365b50de915b71198f674cdaf961ad99246c5500..b92329b187ea87e63ae703849c4de08f8d255a35 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
@@ -2,6 +2,7 @@ package org.briarproject.bramble;
 
 import org.briarproject.bramble.client.ClientModule;
 import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.crypto.CryptoModule;
 import org.briarproject.bramble.data.DataModule;
 import org.briarproject.bramble.db.DatabaseExecutorModule;
@@ -19,6 +20,7 @@ import org.briarproject.bramble.socks.SocksModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 
 import dagger.Module;
 
@@ -26,6 +28,7 @@ import dagger.Module;
 		ClientModule.class,
 		ContactModule.class,
 		CryptoModule.class,
+		CryptoExecutorModule.class,
 		DataModule.class,
 		DatabaseModule.class,
 		DatabaseExecutorModule.class,
@@ -41,13 +44,14 @@ import dagger.Module;
 		SocksModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		VersioningModule.class
 })
 public class BrambleCoreModule {
 
 	public static void initEagerSingletons(BrambleCoreEagerSingletons c) {
 		c.inject(new ContactModule.EagerSingletons());
-		c.inject(new CryptoModule.EagerSingletons());
+		c.inject(new CryptoExecutorModule.EagerSingletons());
 		c.inject(new DatabaseExecutorModule.EagerSingletons());
 		c.inject(new IdentityModule.EagerSingletons());
 		c.inject(new LifecycleModule.EagerSingletons());
@@ -56,5 +60,6 @@ public class BrambleCoreModule {
 		c.inject(new SyncModule.EagerSingletons());
 		c.inject(new SystemModule.EagerSingletons());
 		c.inject(new TransportModule.EagerSingletons());
+		c.inject(new VersioningModule.EagerSingletons());
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/client/ContactGroupFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/client/ContactGroupFactoryImpl.java
index fba2aa745a72fca908df892714ee9305df9ef02b..6b35c9281853010344150d0fb1d6771a6cb3e1ac 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/client/ContactGroupFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/client/ContactGroupFactoryImpl.java
@@ -32,25 +32,25 @@ class ContactGroupFactoryImpl implements ContactGroupFactory {
 	}
 
 	@Override
-	public Group createLocalGroup(ClientId clientId, int clientVersion) {
-		return groupFactory.createGroup(clientId, clientVersion,
+	public Group createLocalGroup(ClientId clientId, int majorVersion) {
+		return groupFactory.createGroup(clientId, majorVersion,
 				LOCAL_GROUP_DESCRIPTOR);
 	}
 
 	@Override
-	public Group createContactGroup(ClientId clientId, int clientVersion,
+	public Group createContactGroup(ClientId clientId, int majorVersion,
 			Contact contact) {
 		AuthorId local = contact.getLocalAuthorId();
 		AuthorId remote = contact.getAuthor().getId();
 		byte[] descriptor = createGroupDescriptor(local, remote);
-		return groupFactory.createGroup(clientId, clientVersion, descriptor);
+		return groupFactory.createGroup(clientId, majorVersion, descriptor);
 	}
 
 	@Override
-	public Group createContactGroup(ClientId clientId, int clientVersion,
+	public Group createContactGroup(ClientId clientId, int majorVersion,
 			AuthorId authorId1, AuthorId authorId2) {
 		byte[] descriptor = createGroupDescriptor(authorId1, authorId2);
-		return groupFactory.createGroup(clientId, clientVersion, descriptor);
+		return groupFactory.createGroup(clientId, majorVersion, descriptor);
 	}
 
 	private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab0e4114f5672469fd0c26b8061389c1e1b7172c
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoExecutorModule.java
@@ -0,0 +1,67 @@
+package org.briarproject.bramble.crypto;
+
+import org.briarproject.bramble.TimeLoggingExecutor;
+import org.briarproject.bramble.api.crypto.CryptoExecutor;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+@Module
+public class CryptoExecutorModule {
+
+	public static class EagerSingletons {
+		@Inject
+		@CryptoExecutor
+		ExecutorService cryptoExecutor;
+	}
+
+	/**
+	 * The maximum number of executor threads.
+	 * <p>
+	 * The number of available processors can change during the lifetime of the
+	 * JVM, so this is just a reasonable guess.
+	 */
+	private static final int MAX_EXECUTOR_THREADS =
+			Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
+
+	private final ExecutorService cryptoExecutor;
+
+	public CryptoExecutorModule() {
+		// Use an unbounded queue
+		BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
+		// Discard tasks that are submitted during shutdown
+		RejectedExecutionHandler policy =
+				new ThreadPoolExecutor.DiscardPolicy();
+		// Create a limited # of threads and keep them in the pool for 60 secs
+		cryptoExecutor = new TimeLoggingExecutor("CryptoExecutor", 0,
+				MAX_EXECUTOR_THREADS, 60, SECONDS, queue, policy);
+	}
+
+	@Provides
+	@Singleton
+	@CryptoExecutor
+	ExecutorService provideCryptoExecutorService(
+			LifecycleManager lifecycleManager) {
+		lifecycleManager.registerForShutdown(cryptoExecutor);
+		return cryptoExecutor;
+	}
+
+	@Provides
+	@CryptoExecutor
+	Executor provideCryptoExecutor() {
+		return cryptoExecutor;
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
index 25d24995d5ddf45b5a9aefea69fe1c5c101d0acd..680391bd001e41389c941f30663fcbb7994dfa63 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
@@ -1,64 +1,24 @@
 package org.briarproject.bramble.crypto;
 
-import org.briarproject.bramble.TimeLoggingExecutor;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.crypto.CryptoExecutor;
 import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
 import org.briarproject.bramble.api.crypto.StreamDecrypterFactory;
 import org.briarproject.bramble.api.crypto.StreamEncrypterFactory;
 import org.briarproject.bramble.api.crypto.TransportCrypto;
-import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.system.SecureRandomProvider;
 
 import java.security.SecureRandom;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.RejectedExecutionHandler;
-import java.util.concurrent.ThreadPoolExecutor;
 
-import javax.inject.Inject;
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import dagger.Module;
 import dagger.Provides;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
-
 @Module
 public class CryptoModule {
 
-	public static class EagerSingletons {
-		@Inject
-		@CryptoExecutor
-		ExecutorService cryptoExecutor;
-	}
-
-	/**
-	 * The maximum number of executor threads.
-	 * <p>
-	 * The number of available processors can change during the lifetime of the
-	 * JVM, so this is just a reasonable guess.
-	 */
-	private static final int MAX_EXECUTOR_THREADS =
-			Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
-
-	private final ExecutorService cryptoExecutor;
-
-	public CryptoModule() {
-		// Use an unbounded queue
-		BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
-		// Discard tasks that are submitted during shutdown
-		RejectedExecutionHandler policy =
-				new ThreadPoolExecutor.DiscardPolicy();
-		// Create a limited # of threads and keep them in the pool for 60 secs
-		cryptoExecutor = new TimeLoggingExecutor("CryptoExecutor", 0,
-				MAX_EXECUTOR_THREADS, 60, SECONDS, queue, policy);
-	}
-
 	@Provides
 	AuthenticatedCipher provideAuthenticatedCipher() {
 		return new XSalsa20Poly1305AuthenticatedCipher();
@@ -103,21 +63,6 @@ public class CryptoModule {
 		return keyAgreementCrypto;
 	}
 
-	@Provides
-	@Singleton
-	@CryptoExecutor
-	ExecutorService getCryptoExecutorService(
-			LifecycleManager lifecycleManager) {
-		lifecycleManager.registerForShutdown(cryptoExecutor);
-		return cryptoExecutor;
-	}
-
-	@Provides
-	@CryptoExecutor
-	Executor getCryptoExecutor() {
-		return cryptoExecutor;
-	}
-
 	@Provides
 	SecureRandom getSecureRandom(CryptoComponent crypto) {
 		return crypto.getSecureRandom();
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 0c3b2b1bccb0681af813a7e0582ac5bcc7766f52..f20c0da5edf4ad11d411b959139cadf36ea4d7e5 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
@@ -266,7 +266,8 @@ interface Database<T> {
 	 * <p/>
 	 * Read-only.
 	 */
-	Collection<Group> getGroups(T txn, ClientId c) throws DbException;
+	Collection<Group> getGroups(T txn, ClientId c, int majorVersion)
+			throws DbException;
 
 	/**
 	 * Returns the given group's visibility to the given contact, or
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 aaab4b9dd8ae51dbaf516a757066ec3d23e7f7ba..1534a9d81bfb47d68d03f4ab90620f2b1397d0b4 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
@@ -435,10 +435,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	}
 
 	@Override
-	public Collection<Group> getGroups(Transaction transaction, ClientId c)
-			throws DbException {
+	public Collection<Group> getGroups(Transaction transaction, ClientId c,
+			int majorVersion) throws DbException {
 		T txn = unbox(transaction);
-		return db.getGroups(txn, c);
+		return db.getGroups(txn, c, majorVersion);
 	}
 
 	@Override
@@ -466,6 +466,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getLocalAuthors(txn);
 	}
 
+	@Override
+	public Collection<MessageId> getMessageIds(Transaction transaction,
+			GroupId g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.getMessageIds(txn, g);
+	}
+
 	@Override
 	public Collection<MessageId> getMessagesToValidate(Transaction transaction)
 			throws DbException {
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 dd0e928a8569c2c52c985fea2a9acb87470ae267..7115808e289bd8fc51a871b10970f70bc7579605 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
@@ -74,7 +74,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	// Package access for testing
-	static final int CODE_SCHEMA_VERSION = 37;
+	static final int CODE_SCHEMA_VERSION = 38;
 
 	// Rotation period offsets for incoming transport keys
 	private static final int OFFSET_PREV = -1;
@@ -117,6 +117,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			"CREATE TABLE groups"
 					+ " (groupId _HASH NOT NULL,"
 					+ " clientId _STRING NOT NULL,"
+					+ " majorVersion INT NOT NULL,"
 					+ " descriptor _BINARY NOT NULL,"
 					+ " PRIMARY KEY (groupId))";
 
@@ -275,9 +276,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 			"CREATE INDEX IF NOT EXISTS contactsByAuthorId"
 					+ " ON contacts (authorId)";
 
-	private static final String INDEX_GROUPS_BY_CLIENT_ID =
-			"CREATE INDEX IF NOT EXISTS groupsByClientId"
-					+ " ON groups (clientId)";
+	private static final String INDEX_GROUPS_BY_CLIENT_ID_MAJOR_VERSION =
+			"CREATE INDEX IF NOT EXISTS groupsByClientIdMajorVersion"
+					+ " ON groups (clientId, majorVersion)";
 
 	private static final String INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE =
 			"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
@@ -444,7 +445,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			s = txn.createStatement();
 			s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
-			s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
+			s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID_MAJOR_VERSION);
 			s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
 			s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
 			s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
@@ -612,12 +613,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 	public void addGroup(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "INSERT INTO groups (groupId, clientId, descriptor)"
-					+ " VALUES (?, ?, ?)";
+			String sql = "INSERT INTO groups"
+					+ " (groupId, clientId, majorVersion, descriptor)"
+					+ " VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getId().getBytes());
 			ps.setString(2, g.getClientId().getString());
-			ps.setBytes(3, g.getDescriptor());
+			ps.setInt(3, g.getMajorVersion());
+			ps.setBytes(4, g.getDescriptor());
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -1346,17 +1349,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT clientId, descriptor FROM groups"
-					+ " WHERE groupId = ?";
+			String sql = "SELECT clientId, majorVersion, descriptor"
+					+ " FROM groups WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			rs = ps.executeQuery();
 			if (!rs.next()) throw new DbStateException();
 			ClientId clientId = new ClientId(rs.getString(1));
-			byte[] descriptor = rs.getBytes(2);
+			int majorVersion = rs.getInt(2);
+			byte[] descriptor = rs.getBytes(3);
 			rs.close();
 			ps.close();
-			return new Group(g, clientId, descriptor);
+			return new Group(g, clientId, majorVersion, descriptor);
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1365,21 +1369,22 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	@Override
-	public Collection<Group> getGroups(Connection txn, ClientId c)
-			throws DbException {
+	public Collection<Group> getGroups(Connection txn, ClientId c,
+			int majorVersion) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT groupId, descriptor FROM groups"
-					+ " WHERE clientId = ?";
+					+ " WHERE clientId = ? AND majorVersion = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setString(1, c.getString());
+			ps.setInt(2, majorVersion);
 			rs = ps.executeQuery();
 			List<Group> groups = new ArrayList<>();
 			while (rs.next()) {
 				GroupId id = new GroupId(rs.getBytes(1));
 				byte[] descriptor = rs.getBytes(2);
-				groups.add(new Group(id, c, descriptor));
+				groups.add(new Group(id, c, majorVersion, descriptor));
 			}
 			rs.close();
 			ps.close();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/properties/PropertiesModule.java b/bramble-core/src/main/java/org/briarproject/bramble/properties/PropertiesModule.java
index 7f7da60cec2b8574786fa8ab63a901b676fdb940..866f95da2643b998e9e4583c90bec6ca7cc896a8 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/properties/PropertiesModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/properties/PropertiesModule.java
@@ -7,6 +7,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -15,6 +16,8 @@ import dagger.Module;
 import dagger.Provides;
 
 import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
+import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION;
+import static org.briarproject.bramble.api.properties.TransportPropertyManager.MINOR_VERSION;
 
 @Module
 public class PropertiesModule {
@@ -33,7 +36,8 @@ public class PropertiesModule {
 			Clock clock) {
 		TransportPropertyValidator validator = new TransportPropertyValidator(
 				clientHelper, metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
@@ -42,11 +46,14 @@ public class PropertiesModule {
 	TransportPropertyManager getTransportPropertyManager(
 			LifecycleManager lifecycleManager,
 			ValidationManager validationManager, ContactManager contactManager,
+			ClientVersioningManager clientVersioningManager,
 			TransportPropertyManagerImpl transportPropertyManager) {
 		lifecycleManager.registerClient(transportPropertyManager);
-		validationManager.registerIncomingMessageHook(CLIENT_ID,
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
 				transportPropertyManager);
 		contactManager.registerContactHook(transportPropertyManager);
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, transportPropertyManager);
 		return transportPropertyManager;
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
index 1296f03bcb1bb5f4b2d1c43b2de4cfb215121412..4b4c360dbd4c2a07c02860e9427c17b8a75a9888 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java
@@ -19,12 +19,15 @@ import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.InvalidMessageException;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -34,15 +37,14 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
-
 @Immutable
 @NotNullByDefault
 class TransportPropertyManagerImpl implements TransportPropertyManager,
-		Client, ContactHook, IncomingMessageHook {
+		Client, ContactHook, ClientVersioningHook, IncomingMessageHook {
 
 	private final DatabaseComponent db;
 	private final ClientHelper clientHelper;
+	private final ClientVersioningManager clientVersioningManager;
 	private final MetadataParser metadataParser;
 	private final ContactGroupFactory contactGroupFactory;
 	private final Clock clock;
@@ -50,22 +52,25 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 
 	@Inject
 	TransportPropertyManagerImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MetadataParser metadataParser,
+			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser,
 			ContactGroupFactory contactGroupFactory, Clock clock) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.metadataParser = metadataParser;
 		this.contactGroupFactory = contactGroupFactory;
 		this.clock = clock;
 		localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
-				CLIENT_VERSION);
+				MAJOR_VERSION);
 	}
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
 		if (db.containsGroup(txn, localGroup.getId())) return;
 		db.addGroup(txn, localGroup);
-		// Ensure we've set things up for any pre-existing contacts
+		// Set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
@@ -73,11 +78,11 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	public void addingContact(Transaction txn, Contact c) throws DbException {
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
-		// Return if we've already set things up for this contact
-		if (db.containsGroup(txn, g.getId())) return;
-		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		// Apply the client's visibility to the contact group
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
 		// Copy the latest local properties into the group
 		Map<TransportId, TransportProperties> local = getLocalProperties(txn);
 		for (Entry<TransportId, TransportProperties> e : local.entrySet()) {
@@ -91,6 +96,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
 	@Override
 	public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
 			throws DbException, InvalidMessageException {
@@ -289,7 +302,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 
 	private Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, c);
+				MAJOR_VERSION, c);
 	}
 
 	private void storeMessage(Transaction txn, GroupId g, TransportId t,
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/GroupFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/GroupFactoryImpl.java
index e7bc4e16e6dca75ee7622f0d42c21d69a56d02b1..5a46a4aad5287d4e896778834681b703ae0336aa 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/GroupFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/GroupFactoryImpl.java
@@ -28,12 +28,12 @@ class GroupFactoryImpl implements GroupFactory {
 	}
 
 	@Override
-	public Group createGroup(ClientId c, int clientVersion, byte[] descriptor) {
-		byte[] clientVersionBytes = new byte[INT_32_BYTES];
-		ByteUtils.writeUint32(clientVersion, clientVersionBytes, 0);
+	public Group createGroup(ClientId c, int majorVersion, byte[] descriptor) {
+		byte[] majorVersionBytes = new byte[INT_32_BYTES];
+		ByteUtils.writeUint32(majorVersion, majorVersionBytes, 0);
 		byte[] hash = crypto.hash(LABEL, new byte[] {FORMAT_VERSION},
-				StringUtils.toUtf8(c.getString()), clientVersionBytes,
+				StringUtils.toUtf8(c.getString()), majorVersionBytes,
 				descriptor);
-		return new Group(new GroupId(hash), c, descriptor);
+		return new Group(new GroupId(hash), c, majorVersion, descriptor);
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/ValidationManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/ValidationManagerImpl.java
index a9c79c33215ca42702a6fed86fbff31429adc527..b8438530579997fc9cad4a33bec8633785dfc502 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/ValidationManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/ValidationManagerImpl.java
@@ -20,6 +20,7 @@ import org.briarproject.bramble.api.sync.MessageFactory;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
+import org.briarproject.bramble.api.versioning.ClientMajorVersion;
 
 import java.util.Collection;
 import java.util.LinkedList;
@@ -51,8 +52,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	private final DatabaseComponent db;
 	private final Executor dbExecutor, validationExecutor;
 	private final MessageFactory messageFactory;
-	private final Map<ClientId, MessageValidator> validators;
-	private final Map<ClientId, IncomingMessageHook> hooks;
+	private final Map<ClientMajorVersion, MessageValidator> validators;
+	private final Map<ClientMajorVersion, IncomingMessageHook> hooks;
 	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	@Inject
@@ -81,14 +82,15 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	}
 
 	@Override
-	public void registerMessageValidator(ClientId c, MessageValidator v) {
-		validators.put(c, v);
+	public void registerMessageValidator(ClientId c, int majorVersion,
+			MessageValidator v) {
+		validators.put(new ClientMajorVersion(c, majorVersion), v);
 	}
 
 	@Override
-	public void registerIncomingMessageHook(ClientId c,
+	public void registerIncomingMessageHook(ClientId c, int majorVersion,
 			IncomingMessageHook hook) {
-		hooks.put(c, hook);
+		hooks.put(new ClientMajorVersion(c, majorVersion), hook);
 	}
 
 	private void validateOutstandingMessagesAsync() {
@@ -199,9 +201,11 @@ class ValidationManagerImpl implements ValidationManager, Service,
 						Message m = messageFactory.createMessage(id, raw);
 						Group g = db.getGroup(txn, m.getGroupId());
 						ClientId c = g.getClientId();
+						int majorVersion = g.getMajorVersion();
 						Metadata meta =
 								db.getMessageMetadataForValidator(txn, id);
-						DeliveryResult result = deliverMessage(txn, m, c, meta);
+						DeliveryResult result =
+								deliverMessage(txn, m, c, majorVersion, meta);
 						if (result.valid) {
 							pending.addAll(getPendingDependents(txn, id));
 							if (result.share) {
@@ -237,14 +241,16 @@ class ValidationManagerImpl implements ValidationManager, Service,
 
 	@ValidationExecutor
 	private void validateMessage(Message m, Group g) {
-		MessageValidator v = validators.get(g.getClientId());
+		ClientMajorVersion cv =
+				new ClientMajorVersion(g.getClientId(), g.getMajorVersion());
+		MessageValidator v = validators.get(cv);
 		if (v == null) {
-			if (LOG.isLoggable(WARNING))
-				LOG.warning("No validator for " + g.getClientId().getString());
+			if (LOG.isLoggable(WARNING)) LOG.warning("No validator for " + cv);
 		} else {
 			try {
 				MessageContext context = v.validateMessage(m, g);
-				storeMessageContextAsync(m, g.getClientId(), context);
+				storeMessageContextAsync(m, g.getClientId(),
+						g.getMajorVersion(), context);
 			} catch (InvalidMessageException e) {
 				if (LOG.isLoggable(INFO))
 					LOG.log(INFO, e.toString(), e);
@@ -256,12 +262,13 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	}
 
 	private void storeMessageContextAsync(Message m, ClientId c,
-			MessageContext result) {
-		dbExecutor.execute(() -> storeMessageContext(m, c, result));
+			int majorVersion, MessageContext result) {
+		dbExecutor.execute(() ->
+				storeMessageContext(m, c, majorVersion, result));
 	}
 
 	@DatabaseExecutor
-	private void storeMessageContext(Message m, ClientId c,
+	private void storeMessageContext(Message m, ClientId c, int majorVersion,
 			MessageContext context) {
 		try {
 			MessageId id = m.getId();
@@ -292,7 +299,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
 					Metadata meta = context.getMetadata();
 					db.mergeMessageMetadata(txn, id, meta);
 					if (allDelivered) {
-						DeliveryResult result = deliverMessage(txn, m, c, meta);
+						DeliveryResult result =
+								deliverMessage(txn, m, c, majorVersion, meta);
 						if (result.valid) {
 							pending = getPendingDependents(txn, id);
 							if (result.share) {
@@ -324,10 +332,11 @@ class ValidationManagerImpl implements ValidationManager, Service,
 
 	@DatabaseExecutor
 	private DeliveryResult deliverMessage(Transaction txn, Message m,
-			ClientId c, Metadata meta) throws DbException {
+			ClientId c, int majorVersion, Metadata meta) throws DbException {
 		// Deliver the message to the client if it's registered a hook
 		boolean shareMsg = false;
-		IncomingMessageHook hook = hooks.get(c);
+		ClientMajorVersion cv = new ClientMajorVersion(c, majorVersion);
+		IncomingMessageHook hook = hooks.get(cv);
 		if (hook != null) {
 			try {
 				shareMsg = hook.incomingMessage(txn, m, meta);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..e68ecc4688f59ece7843eb20f5a464ffcbd79aad
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java
@@ -0,0 +1,10 @@
+package org.briarproject.bramble.versioning;
+
+interface ClientVersioningConstants {
+
+	// Metadata keys
+	String MSG_KEY_UPDATE_VERSION = "version";
+	String MSG_KEY_LOCAL = "local";
+	String GROUP_KEY_CONTACT_ID = "contactId";
+}
+
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c73abfe3787137a4a73231d86a27f32379be9d6
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java
@@ -0,0 +1,622 @@
+package org.briarproject.bramble.versioning;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+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.Transaction;
+import org.briarproject.bramble.api.lifecycle.Service;
+import org.briarproject.bramble.api.lifecycle.ServiceException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Client;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.InvalidMessageException;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientMajorVersion;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static java.util.Collections.emptyList;
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
+
+@NotNullByDefault
+class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
+		Service, ContactHook, IncomingMessageHook {
+
+	private final DatabaseComponent db;
+	private final ClientHelper clientHelper;
+	private final ContactGroupFactory contactGroupFactory;
+	private final Clock clock;
+	private final Group localGroup;
+
+	private final List<ClientVersion> clients = new CopyOnWriteArrayList<>();
+	private final Map<ClientMajorVersion, ClientVersioningHook> hooks =
+			new ConcurrentHashMap<>();
+
+	@Inject
+	ClientVersioningManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ContactGroupFactory contactGroupFactory, Clock clock) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.contactGroupFactory = contactGroupFactory;
+		this.clock = clock;
+		localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
+				MAJOR_VERSION);
+	}
+
+	@Override
+	public void registerClient(ClientId clientId, int majorVersion,
+			int minorVersion, ClientVersioningHook hook) {
+		ClientMajorVersion cv = new ClientMajorVersion(clientId, majorVersion);
+		clients.add(new ClientVersion(cv, minorVersion));
+		hooks.put(cv, hook);
+	}
+
+	@Override
+	public Visibility getClientVisibility(Transaction txn, ContactId contactId,
+			ClientId clientId, int majorVersion) throws DbException {
+		try {
+			Contact contact = db.getContact(txn, contactId);
+			Group g = getContactGroup(contact);
+			// Contact may be in the process of being added or removed, so
+			// contact group may not exist
+			if (!db.containsGroup(txn, g.getId())) return INVISIBLE;
+			LatestUpdates latest = findLatestUpdates(txn, g.getId());
+			if (latest.local == null) throw new DbException();
+			if (latest.remote == null) return INVISIBLE;
+			Update localUpdate = loadUpdate(txn, latest.local.messageId);
+			Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
+			Map<ClientMajorVersion, Visibility> visibilities =
+					getVisibilities(localUpdate.states, remoteUpdate.states);
+			ClientMajorVersion cv =
+					new ClientMajorVersion(clientId, majorVersion);
+			Visibility v = visibilities.get(cv);
+			return v == null ? INVISIBLE : v;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+		if (db.containsGroup(txn, localGroup.getId())) return;
+		db.addGroup(txn, localGroup);
+		// Set things up for any pre-existing contacts
+		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
+	}
+
+	@Override
+	public void startService() throws ServiceException {
+		List<ClientVersion> versions = new ArrayList<>(clients);
+		Collections.sort(versions);
+		try {
+			Transaction txn = db.startTransaction(false);
+			try {
+				if (updateClientVersions(txn, versions)) {
+					for (Contact c : db.getContacts(txn))
+						clientVersionsUpdated(txn, c, versions);
+				}
+				db.commitTransaction(txn);
+			} finally {
+				db.endTransaction(txn);
+			}
+		} catch (DbException e) {
+			throw new ServiceException(e);
+		}
+	}
+
+	@Override
+	public void stopService() throws ServiceException {
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group and share it with the contact
+		Group g = getContactGroup(c);
+		db.addGroup(txn, g);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		// Attach the contact ID to the group
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
+		try {
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+		// Create and store the first local update
+		List<ClientVersion> versions = new ArrayList<>(clients);
+		Collections.sort(versions);
+		storeFirstUpdate(txn, g.getId(), versions);
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		db.removeGroup(txn, getContactGroup(c));
+	}
+
+	@Override
+	public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
+			throws DbException, InvalidMessageException {
+		try {
+			// Parse the new remote update
+			Update newRemoteUpdate = parseUpdate(clientHelper.toList(m));
+			List<ClientState> newRemoteStates = newRemoteUpdate.states;
+			long newRemoteUpdateVersion = newRemoteUpdate.updateVersion;
+			// Find the latest local and remote updates, if any
+			LatestUpdates latest = findLatestUpdates(txn, m.getGroupId());
+			// If this update is obsolete, delete it and return
+			if (latest.remote != null
+					&& latest.remote.updateVersion > newRemoteUpdateVersion) {
+				db.deleteMessage(txn, m.getId());
+				db.deleteMessageMetadata(txn, m.getId());
+				return false;
+			}
+			// Load and parse the latest local update
+			if (latest.local == null) throw new DbException();
+			Update oldLocalUpdate = loadUpdate(txn, latest.local.messageId);
+			List<ClientState> oldLocalStates = oldLocalUpdate.states;
+			long oldLocalUpdateVersion = oldLocalUpdate.updateVersion;
+			// Load and parse the previous remote update, if any
+			List<ClientState> oldRemoteStates;
+			if (latest.remote == null) {
+				oldRemoteStates = emptyList();
+			} else {
+				oldRemoteStates =
+						loadUpdate(txn, latest.remote.messageId).states;
+				// Delete the previous remote update
+				db.deleteMessage(txn, latest.remote.messageId);
+				db.deleteMessageMetadata(txn, latest.remote.messageId);
+			}
+			// Update the local states from the remote states if necessary
+			List<ClientState> newLocalStates = updateStatesFromRemoteStates(
+					oldLocalStates, newRemoteStates);
+			if (!oldLocalStates.equals(newLocalStates)) {
+				// Delete the latest local update
+				db.deleteMessage(txn, latest.local.messageId);
+				db.deleteMessageMetadata(txn, latest.local.messageId);
+				// Store a new local update
+				storeUpdate(txn, m.getGroupId(), newLocalStates,
+						oldLocalUpdateVersion + 1);
+			}
+			// Calculate the old and new client visibilities
+			Map<ClientMajorVersion, Visibility> before =
+					getVisibilities(oldLocalStates, oldRemoteStates);
+			Map<ClientMajorVersion, Visibility> after =
+					getVisibilities(newLocalStates, newRemoteStates);
+			// Call hooks for any visibilities that have changed
+			if (!before.equals(after)) {
+				Contact c = getContact(txn, m.getGroupId());
+				callVisibilityHooks(txn, c, before, after);
+			}
+		} catch (FormatException e) {
+			throw new InvalidMessageException(e);
+		}
+		return false;
+	}
+
+	private void storeClientVersions(Transaction txn,
+			List<ClientVersion> versions) throws DbException {
+		long now = clock.currentTimeMillis();
+		BdfList body = encodeClientVersions(versions);
+		try {
+			Message m = clientHelper.createMessage(localGroup.getId(), now,
+					body);
+			db.addLocalMessage(txn, m, new Metadata(), false);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	private BdfList encodeClientVersions(List<ClientVersion> versions) {
+		BdfList encoded = new BdfList();
+		for (ClientVersion cv : versions) encoded.add(encodeClientVersion(cv));
+		return encoded;
+	}
+
+	private BdfList encodeClientVersion(ClientVersion cv) {
+		return BdfList.of(cv.majorVersion.getClientId().getString(),
+				cv.majorVersion.getMajorVersion(), cv.minorVersion);
+	}
+
+	/**
+	 * Stores the local client versions and returns true if an update needs to
+	 * be sent to contacts.
+	 */
+	private boolean updateClientVersions(Transaction txn,
+			List<ClientVersion> newVersions) throws DbException {
+		Collection<MessageId> ids = db.getMessageIds(txn, localGroup.getId());
+		if (ids.isEmpty()) {
+			storeClientVersions(txn, newVersions);
+			return true;
+		}
+		if (ids.size() != 1) throw new DbException();
+		MessageId m = ids.iterator().next();
+		List<ClientVersion> oldVersions = loadClientVersions(txn, m);
+		if (oldVersions.equals(newVersions)) return false;
+		db.removeMessage(txn, m);
+		storeClientVersions(txn, newVersions);
+		return true;
+	}
+
+	private List<ClientVersion> loadClientVersions(Transaction txn,
+			MessageId m) throws DbException {
+		try {
+			BdfList body = clientHelper.getMessageAsList(txn, m);
+			if (body == null) throw new DbException();
+			return parseClientVersions(body);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private List<ClientVersion> parseClientVersions(BdfList body)
+			throws FormatException {
+		int size = body.size();
+		List<ClientVersion> parsed = new ArrayList<>(size);
+		for (int i = 0; i < size; i++) {
+			BdfList cv = body.getList(i);
+			ClientId clientId = new ClientId(cv.getString(0));
+			int majorVersion = cv.getLong(1).intValue();
+			int minorVersion = cv.getLong(2).intValue();
+			parsed.add(new ClientVersion(clientId, majorVersion,
+					minorVersion));
+		}
+		return parsed;
+	}
+
+	private void clientVersionsUpdated(Transaction txn, Contact c,
+			List<ClientVersion> versions) throws DbException {
+		try {
+			// Find the latest local and remote updates
+			Group g = getContactGroup(c);
+			LatestUpdates latest = findLatestUpdates(txn, g.getId());
+			// Load and parse the latest local update
+			if (latest.local == null) throw new DbException();
+			Update oldLocalUpdate = loadUpdate(txn, latest.local.messageId);
+			List<ClientState> oldLocalStates = oldLocalUpdate.states;
+			long oldLocalUpdateVersion = oldLocalUpdate.updateVersion;
+			// Load and parse the latest remote update, if any
+			List<ClientState> remoteStates;
+			if (latest.remote == null) remoteStates = emptyList();
+			else remoteStates = loadUpdate(txn, latest.remote.messageId).states;
+			// Update the local states if necessary
+			List<ClientState> newLocalStates =
+					updateStatesFromLocalVersions(oldLocalStates, versions);
+			newLocalStates = updateStatesFromRemoteStates(newLocalStates,
+					remoteStates);
+			if (!oldLocalStates.equals(newLocalStates)) {
+				// Delete the latest local update
+				db.deleteMessage(txn, latest.local.messageId);
+				db.deleteMessageMetadata(txn, latest.local.messageId);
+				// Store a new local update
+				storeUpdate(txn, g.getId(), newLocalStates,
+						oldLocalUpdateVersion + 1);
+			}
+			// Calculate the old and new client visibilities
+			Map<ClientMajorVersion, Visibility> before =
+					getVisibilities(oldLocalStates, remoteStates);
+			Map<ClientMajorVersion, Visibility> after =
+					getVisibilities(newLocalStates, remoteStates);
+			// Call hooks for any visibilities that have changed
+			callVisibilityHooks(txn, c, before, after);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private Group getContactGroup(Contact c) {
+		return contactGroupFactory.createContactGroup(CLIENT_ID,
+				MAJOR_VERSION, c);
+	}
+
+	private LatestUpdates findLatestUpdates(Transaction txn, GroupId g)
+			throws DbException, FormatException {
+		Map<MessageId, BdfDictionary> metadata =
+				clientHelper.getMessageMetadataAsDictionary(txn, g);
+		LatestUpdate local = null, remote = null;
+		for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
+			BdfDictionary meta = e.getValue();
+			long updateVersion = meta.getLong(MSG_KEY_UPDATE_VERSION);
+			if (meta.getBoolean(MSG_KEY_LOCAL))
+				local = new LatestUpdate(e.getKey(), updateVersion);
+			else remote = new LatestUpdate(e.getKey(), updateVersion);
+		}
+		return new LatestUpdates(local, remote);
+	}
+
+	private Update loadUpdate(Transaction txn, MessageId m) throws DbException {
+		try {
+			BdfList body = clientHelper.getMessageAsList(txn, m);
+			if (body == null) throw new DbException();
+			return parseUpdate(body);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private Update parseUpdate(BdfList body) throws FormatException {
+		List<ClientState> states = parseClientStates(body);
+		long updateVersion = parseUpdateVersion(body);
+		return new Update(states, updateVersion);
+	}
+
+	private List<ClientState> parseClientStates(BdfList body)
+			throws FormatException {
+		// Client states, update version
+		BdfList states = body.getList(0);
+		int size = states.size();
+		List<ClientState> parsed = new ArrayList<>(size);
+		for (int i = 0; i < size; i++)
+			parsed.add(parseClientState(states.getList(i)));
+		return parsed;
+	}
+
+	private ClientState parseClientState(BdfList clientState)
+			throws FormatException {
+		// Client ID, major version, minor version, active
+		ClientId clientId = new ClientId(clientState.getString(0));
+		int majorVersion = clientState.getLong(1).intValue();
+		int minorVersion = clientState.getLong(2).intValue();
+		boolean active = clientState.getBoolean(3);
+		return new ClientState(clientId, majorVersion, minorVersion, active);
+	}
+
+	private long parseUpdateVersion(BdfList body) throws FormatException {
+		// Client states, update version
+		return body.getLong(1);
+	}
+
+	private List<ClientState> updateStatesFromLocalVersions(
+			List<ClientState> oldStates, List<ClientVersion> newVersions) {
+		Map<ClientMajorVersion, ClientState> oldMap = new HashMap<>();
+		for (ClientState cs : oldStates) oldMap.put(cs.majorVersion, cs);
+		List<ClientState> newStates = new ArrayList<>(newVersions.size());
+		for (ClientVersion newVersion : newVersions) {
+			ClientState oldState = oldMap.get(newVersion.majorVersion);
+			boolean active = oldState != null && oldState.active;
+			newStates.add(new ClientState(newVersion.majorVersion,
+					newVersion.minorVersion, active));
+		}
+		return newStates;
+	}
+
+	private void storeUpdate(Transaction txn, GroupId g,
+			List<ClientState> states, long updateVersion) throws DbException {
+		try {
+			BdfList body = encodeUpdate(states, updateVersion);
+			long now = clock.currentTimeMillis();
+			Message m = clientHelper.createMessage(g, now, body);
+			BdfDictionary meta = new BdfDictionary();
+			meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
+			meta.put(MSG_KEY_LOCAL, true);
+			clientHelper.addLocalMessage(txn, m, meta, true);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private BdfList encodeUpdate(List<ClientState> states, long updateVersion) {
+		BdfList encoded = new BdfList();
+		for (ClientState cs : states) encoded.add(encodeClientState(cs));
+		return BdfList.of(encoded, updateVersion);
+	}
+
+	private BdfList encodeClientState(ClientState cs) {
+		return BdfList.of(cs.majorVersion.getClientId().getString(),
+				cs.majorVersion.getMajorVersion(), cs.minorVersion, cs.active);
+	}
+
+	private Map<ClientMajorVersion, Visibility> getVisibilities(
+			List<ClientState> localStates, List<ClientState> remoteStates) {
+		Map<ClientMajorVersion, ClientState> remoteMap = new HashMap<>();
+		for (ClientState cs : remoteStates) remoteMap.put(cs.majorVersion, cs);
+		Map<ClientMajorVersion, Visibility> visibilities = new HashMap<>();
+		for (ClientState local : localStates) {
+			ClientState remote = remoteMap.get(local.majorVersion);
+			if (remote == null) visibilities.put(local.majorVersion, INVISIBLE);
+			else if (remote.active)
+				visibilities.put(local.majorVersion, SHARED);
+			else visibilities.put(local.majorVersion, VISIBLE);
+		}
+		return visibilities;
+	}
+
+	private void callVisibilityHooks(Transaction txn, Contact c,
+			Map<ClientMajorVersion, Visibility> before,
+			Map<ClientMajorVersion, Visibility> after) throws DbException {
+		Set<ClientMajorVersion> keys = new TreeSet<>();
+		keys.addAll(before.keySet());
+		keys.addAll(after.keySet());
+		for (ClientMajorVersion cv : keys) {
+			Visibility vBefore = before.get(cv), vAfter = after.get(cv);
+			if (vAfter == null) {
+				callVisibilityHook(txn, cv, c, INVISIBLE);
+			} else if (vBefore == null || !vBefore.equals(vAfter)) {
+				callVisibilityHook(txn, cv, c, vAfter);
+			}
+		}
+	}
+
+	private void callVisibilityHook(Transaction txn, ClientMajorVersion cv,
+			Contact c, Visibility v) throws DbException {
+		ClientVersioningHook hook = hooks.get(cv);
+		if (hook != null) hook.onClientVisibilityChanging(txn, c, v);
+	}
+
+	private void storeFirstUpdate(Transaction txn, GroupId g,
+			List<ClientVersion> versions) throws DbException {
+		List<ClientState> states = new ArrayList<>(versions.size());
+		for (ClientVersion cv : versions) {
+			states.add(new ClientState(cv.majorVersion, cv.minorVersion,
+					false));
+		}
+		storeUpdate(txn, g, states, 1);
+	}
+
+	private Contact getContact(Transaction txn, GroupId g) throws DbException {
+		try {
+			BdfDictionary meta =
+					clientHelper.getGroupMetadataAsDictionary(txn, g);
+			int id = meta.getLong(GROUP_KEY_CONTACT_ID).intValue();
+			return db.getContact(txn, new ContactId(id));
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private List<ClientState> updateStatesFromRemoteStates(
+			List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
+		Set<ClientMajorVersion> remoteSet = new HashSet<>();
+		for (ClientState cs : remoteStates) remoteSet.add(cs.majorVersion);
+		List<ClientState> newLocalStates =
+				new ArrayList<>(oldLocalStates.size());
+		for (ClientState oldState : oldLocalStates) {
+			boolean active = remoteSet.contains(oldState.majorVersion);
+			newLocalStates.add(new ClientState(oldState.majorVersion,
+					oldState.minorVersion, active));
+		}
+		return newLocalStates;
+	}
+
+	private static class Update {
+
+		private final List<ClientState> states;
+		private final long updateVersion;
+
+		private Update(List<ClientState> states, long updateVersion) {
+			this.states = states;
+			this.updateVersion = updateVersion;
+		}
+	}
+
+	private static class LatestUpdate {
+
+		private final MessageId messageId;
+		private final long updateVersion;
+
+		private LatestUpdate(MessageId messageId, long updateVersion) {
+			this.messageId = messageId;
+			this.updateVersion = updateVersion;
+		}
+	}
+
+	private static class LatestUpdates {
+
+		@Nullable
+		private final LatestUpdate local, remote;
+
+		private LatestUpdates(@Nullable LatestUpdate local,
+				@Nullable LatestUpdate remote) {
+			this.local = local;
+			this.remote = remote;
+		}
+	}
+
+	private static class ClientVersion implements Comparable<ClientVersion> {
+
+		private final ClientMajorVersion majorVersion;
+		private final int minorVersion;
+
+		private ClientVersion(ClientMajorVersion majorVersion,
+				int minorVersion) {
+			this.majorVersion = majorVersion;
+			this.minorVersion = minorVersion;
+		}
+
+		private ClientVersion(ClientId clientId, int majorVersion,
+				int minorVersion) {
+			this(new ClientMajorVersion(clientId, majorVersion), minorVersion);
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o instanceof ClientVersion) {
+				ClientVersion cv = (ClientVersion) o;
+				return majorVersion.equals(cv.majorVersion)
+						&& minorVersion == cv.minorVersion;
+			}
+			return false;
+		}
+
+		@Override
+		public int hashCode() {
+			return majorVersion.hashCode();
+		}
+
+		@Override
+		public int compareTo(ClientVersion cv) {
+			int compare = majorVersion.compareTo(cv.majorVersion);
+			if (compare != 0) return compare;
+			return minorVersion - cv.minorVersion;
+		}
+	}
+
+	private static class ClientState {
+
+		private final ClientMajorVersion majorVersion;
+		private final int minorVersion;
+		private final boolean active;
+
+		private ClientState(ClientMajorVersion majorVersion, int minorVersion,
+				boolean active) {
+			this.majorVersion = majorVersion;
+			this.minorVersion = minorVersion;
+			this.active = active;
+		}
+
+		private ClientState(ClientId clientId, int majorVersion,
+				int minorVersion, boolean active) {
+			this(new ClientMajorVersion(clientId, majorVersion), minorVersion,
+					active);
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o instanceof ClientState) {
+				ClientState cs = (ClientState) o;
+				return majorVersion.equals(cs.majorVersion)
+						&& minorVersion == cs.minorVersion
+						&& active == cs.active;
+			}
+			return false;
+		}
+
+		@Override
+		public int hashCode() {
+			return majorVersion.hashCode();
+		}
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningValidator.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..e2fc20913b996e2565bb6e5d1f66862a32690800
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningValidator.java
@@ -0,0 +1,61 @@
+package org.briarproject.bramble.versioning;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.BdfMessageContext;
+import org.briarproject.bramble.api.client.BdfMessageValidator;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.system.Clock;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH;
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
+
+@Immutable
+@NotNullByDefault
+class ClientVersioningValidator extends BdfMessageValidator {
+
+	ClientVersioningValidator(ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+	}
+
+	@Override
+	protected BdfMessageContext validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+		// Client states, update version
+		checkSize(body, 2);
+		// Client states
+		BdfList states = body.getList(0);
+		int size = states.size();
+		for (int i = 0; i < size; i++) {
+			BdfList clientState = states.getList(i);
+			// Client ID, major version, minor version, active
+			checkSize(clientState, 4);
+			String clientId = clientState.getString(0);
+			checkLength(clientId, 1, MAX_CLIENT_ID_LENGTH);
+			int majorVersion = clientState.getLong(1).intValue();
+			if (majorVersion < 0) throw new FormatException();
+			int minorVersion = clientState.getLong(2).intValue();
+			if (minorVersion < 0) throw new FormatException();
+			clientState.getBoolean(3);
+		}
+		// Update version
+		long updateVersion = body.getLong(1);
+		if (updateVersion < 0) throw new FormatException();
+		// Return the metadata
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
+		meta.put(MSG_KEY_LOCAL, false);
+		return new BdfMessageContext(meta);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/VersioningModule.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/VersioningModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..f018ffd76c72cf7f085dd8dabc396119a81cda48
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/VersioningModule.java
@@ -0,0 +1,56 @@
+package org.briarproject.bramble.versioning;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactManager;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.CLIENT_ID;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.MAJOR_VERSION;
+
+@Module
+public class VersioningModule {
+
+	public static class EagerSingletons {
+		@Inject
+		ClientVersioningManager clientVersioningManager;
+		@Inject
+		ClientVersioningValidator clientVersioningValidator;
+	}
+
+
+	@Provides
+	@Singleton
+	ClientVersioningManager provideClientVersioningManager(
+			ClientVersioningManagerImpl clientVersioningManager,
+			LifecycleManager lifecycleManager, ContactManager contactManager,
+			ValidationManager validationManager) {
+		lifecycleManager.registerClient(clientVersioningManager);
+		lifecycleManager.registerService(clientVersioningManager);
+		contactManager.registerContactHook(clientVersioningManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
+				clientVersioningManager);
+		return clientVersioningManager;
+	}
+
+	@Provides
+	@Singleton
+	ClientVersioningValidator provideClientVersioningValidator(
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock, ValidationManager validationManager) {
+		ClientVersioningValidator validator = new ClientVersioningValidator(
+				clientHelper, metadataEncoder, clock);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
+		return validator;
+	}
+}
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 ae3e203405e8006461a7d979bc7b82ee50dd7085..c228bf67ebc20ab3ad35ce8fa7300cc2ee848bd5 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
@@ -89,6 +89,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 	private final Object txn = new Object();
 	private final ClientId clientId;
+	private final int majorVersion;
 	private final GroupId groupId;
 	private final Group group;
 	private final Author author;
@@ -106,7 +107,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 	public DatabaseComponentImplTest() {
 		clientId = getClientId();
-		group = getGroup(clientId);
+		majorVersion = 123;
+		group = getGroup(clientId, majorVersion);
 		groupId = group.getId();
 		author = getAuthor();
 		localAuthor = getLocalAuthor();
@@ -175,7 +177,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
 			// getGroups()
-			oneOf(database).getGroups(txn, clientId);
+			oneOf(database).getGroups(txn, clientId, majorVersion);
 			will(returnValue(singletonList(group)));
 			// removeGroup()
 			oneOf(database).containsGroup(txn, groupId);
@@ -215,7 +217,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			db.addGroup(transaction, group); // First time - listeners called
 			db.addGroup(transaction, group); // Second time - not called
 			assertEquals(singletonList(group),
-					db.getGroups(transaction, clientId));
+					db.getGroups(transaction, clientId, majorVersion));
 			db.removeGroup(transaction, group);
 			db.removeContact(transaction, contactId);
 			db.removeLocalAuthor(transaction, localAuthor.getId());
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
index 389f7c1c3a5e50b3152f046578b0a038022f3d60..e46a75b68460ba81166b20aabbfcddcbca1fe4b8 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
@@ -267,7 +267,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 		String name = "getGroups(T, ClientId)";
 		benchmark(name, db -> {
 			Connection txn = db.startTransaction();
-			db.getGroups(txn, pickRandom(clientIds));
+			db.getGroups(txn, pickRandom(clientIds), 123);
 			db.commitTransaction(txn);
 		});
 	}
@@ -550,7 +550,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 			contacts.add(db.getContact(txn, c));
 			contactGroups.put(c, new ArrayList<>());
 			for (int j = 0; j < GROUPS_PER_CONTACT; j++) {
-				Group g = getGroup(clientIds.get(j % CLIENTS));
+				Group g = getGroup(clientIds.get(j % CLIENTS), 123);
 				groups.add(g);
 				messageMeta.put(g.getId(), new ArrayList<>());
 				contactGroups.get(c).add(g);
@@ -584,7 +584,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 			}
 		}
 		for (int i = 0; i < LOCAL_GROUPS; i++) {
-			Group g = getGroup(clientIds.get(i % CLIENTS));
+			Group g = getGroup(clientIds.get(i % CLIENTS), 123);
 			groups.add(g);
 			messageMeta.put(g.getId(), new ArrayList<>());
 			groupMessages.put(g.getId(), new ArrayList<>());
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 97268d16b3e551b28525517e9d6fb5c7a76b2f1e..d5b29dc65775931bc492247c265071478f83d1f9 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
@@ -82,6 +82,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 	private final File testDir = TestUtils.getTestDirectory();
 	private final GroupId groupId;
 	private final ClientId clientId;
+	private final int majorVersion;
 	private final Group group;
 	private final Author author;
 	private final LocalAuthor localAuthor;
@@ -96,7 +97,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 	JdbcDatabaseTest() throws Exception {
 		clientId = getClientId();
-		group = getGroup(clientId);
+		majorVersion = 123;
+		group = getGroup(clientId, majorVersion);
 		groupId = group.getId();
 		author = getAuthor();
 		localAuthor = getLocalAuthor();
@@ -1460,7 +1462,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.addMessage(txn, message, PENDING, true, contactId);
 
 		// Add a second group
-		Group group1 = getGroup(clientId);
+		Group group1 = getGroup(clientId, 123);
 		GroupId groupId1 = group1.getId();
 		db.addGroup(txn, group1);
 
@@ -1828,6 +1830,22 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGetGroups() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		assertEquals(emptyList(), db.getGroups(txn, clientId, majorVersion));
+		db.addGroup(txn, group);
+		assertEquals(singletonList(group),
+				db.getGroups(txn, clientId, majorVersion));
+		db.removeGroup(txn, groupId);
+		assertEquals(emptyList(), db.getGroups(txn, clientId, majorVersion));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testExceptionHandling() throws Exception {
 		Database<Connection> db = open(false);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java
index 9702c2d5111d37713fdeff3d1775355f42aa343e..90aba549dba4d350db7706b12d35b80f361ca857 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java
@@ -19,6 +19,7 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.jmock.Expectations;
 import org.junit.Test;
@@ -29,8 +30,9 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import static java.util.Collections.singletonList;
 import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
-import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_VERSION;
+import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
@@ -45,13 +47,15 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 
 	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
 	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	private final MetadataParser metadataParser =
 			context.mock(MetadataParser.class);
 	private final ContactGroupFactory contactGroupFactory =
 			context.mock(ContactGroupFactory.class);
 	private final Clock clock = context.mock(Clock.class);
 
-	private final Group localGroup = getGroup(CLIENT_ID);
+	private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 	private final LocalAuthor localAuthor = getLocalAuthor();
 	private final BdfDictionary fooPropertiesDict = BdfDictionary.of(
 			new BdfEntry("fooKey1", "fooValue1"),
@@ -77,49 +81,41 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	private TransportPropertyManagerImpl createInstance() {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 		}});
 		return new TransportPropertyManagerImpl(db, clientHelper,
-				metadataParser, contactGroupFactory, clock);
+				clientVersioningManager, metadataParser, contactGroupFactory,
+				clock);
 	}
 
 	@Test
 	public void testCreatesGroupsAtStartup() throws Exception {
 		Transaction txn = new Transaction(null, false);
-		Contact contact1 = getContact(true);
-		Contact contact2 = getContact(true);
-		List<Contact> contacts = Arrays.asList(contact1, contact2);
-		Group contactGroup1 = getGroup(CLIENT_ID);
-		Group contactGroup2 = getGroup(CLIENT_ID);
+		Contact contact = getContact(true);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 
 		context.checking(new Expectations() {{
 			oneOf(db).containsGroup(txn, localGroup.getId());
 			will(returnValue(false));
 			oneOf(db).addGroup(txn, localGroup);
 			oneOf(db).getContacts(txn);
-			will(returnValue(contacts));
-			// The first contact's group has already been set up
+			will(returnValue(singletonList(contact)));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact1);
-			will(returnValue(contactGroup1));
-			oneOf(db).containsGroup(txn, contactGroup1.getId());
-			will(returnValue(true));
-			// The second contact's group hasn't been set up
-			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact2);
-			will(returnValue(contactGroup2));
-			oneOf(db).containsGroup(txn, contactGroup2.getId());
-			will(returnValue(false));
-			oneOf(db).addGroup(txn, contactGroup2);
-			oneOf(db).setGroupVisibility(txn, contact2.getId(),
-					contactGroup2.getId(), SHARED);
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(clientVersioningManager).getClientVisibility(txn,
+					contact.getId(), CLIENT_ID, MAJOR_VERSION);
+			will(returnValue(SHARED));
+			oneOf(db).setGroupVisibility(txn, contact.getId(),
+					contactGroup.getId(), SHARED);
 		}});
 		// Copy the latest local properties into the group
 		expectGetLocalProperties(txn);
-		expectStoreMessage(txn, contactGroup2.getId(), "foo", fooPropertiesDict,
+		expectStoreMessage(txn, contactGroup.getId(), "foo", fooPropertiesDict,
 				1, true, true);
-		expectStoreMessage(txn, contactGroup2.getId(), "bar", barPropertiesDict,
+		expectStoreMessage(txn, contactGroup.getId(), "bar", barPropertiesDict,
 				1, true, true);
 
 		TransportPropertyManagerImpl t = createInstance();
@@ -144,16 +140,17 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	public void testCreatesContactGroupWhenAddingContact() throws Exception {
 		Transaction txn = new Transaction(null, false);
 		Contact contact = getContact(true);
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 
 		context.checking(new Expectations() {{
 			// Create the group and share it with the contact
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
-			oneOf(db).containsGroup(txn, contactGroup.getId());
-			will(returnValue(false));
 			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(clientVersioningManager).getClientVisibility(txn,
+					contact.getId(), CLIENT_ID, MAJOR_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, contact.getId(),
 					contactGroup.getId(), SHARED);
 		}});
@@ -172,11 +169,11 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	public void testRemovesGroupWhenRemovingContact() throws Exception {
 		Transaction txn = new Transaction(null, false);
 		Contact contact = getContact(true);
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(db).removeGroup(txn, contactGroup);
 		}});
@@ -307,7 +304,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	@Test
 	public void testStoresRemotePropertiesWithVersion0() throws Exception {
 		Contact contact = getContact(true);
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 		Transaction txn = new Transaction(null, false);
 		Map<TransportId, TransportProperties> properties =
 				new LinkedHashMap<>();
@@ -318,7 +315,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contact.getId());
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 		}});
 		expectStoreMessage(txn, contactGroup.getId(), "foo", fooPropertiesDict,
@@ -421,8 +418,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 		Contact contact3 = getContact(true);
 		List<Contact> contacts =
 				Arrays.asList(contact1, contact2, contact3);
-		Group contactGroup2 = getGroup(CLIENT_ID);
-		Group contactGroup3 = getGroup(CLIENT_ID);
+		Group contactGroup2 = getGroup(CLIENT_ID, MAJOR_VERSION);
+		Group contactGroup3 = getGroup(CLIENT_ID, MAJOR_VERSION);
 		Map<MessageId, BdfDictionary> messageMetadata3 =
 				new LinkedHashMap<>();
 		// A remote update for another transport should be ignored
@@ -456,14 +453,14 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 			// First contact: skipped because not active
 			// Second contact: no updates
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact2);
+					MAJOR_VERSION, contact2);
 			will(returnValue(contactGroup2));
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
 					contactGroup2.getId());
 			will(returnValue(Collections.emptyMap()));
 			// Third contact: returns an update
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact3);
+					MAJOR_VERSION, contact3);
 			will(returnValue(contactGroup3));
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
 					contactGroup3.getId());
@@ -524,7 +521,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	public void testMergingNewPropertiesCreatesUpdate() throws Exception {
 		Transaction txn = new Transaction(null, false);
 		Contact contact = getContact(true);
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 
 		context.checking(new Expectations() {{
 			oneOf(db).startTransaction(false);
@@ -538,9 +535,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 					fooPropertiesDict, 1, true, false);
 			// Store the new properties in each contact's group, version 1
 			oneOf(db).getContacts(txn);
-			will(returnValue(Collections.singletonList(contact)));
+			will(returnValue(singletonList(contact)));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
 					contactGroup.getId());
@@ -559,7 +556,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 	public void testMergingUpdatedPropertiesCreatesUpdate() throws Exception {
 		Transaction txn = new Transaction(null, false);
 		Contact contact = getContact(true);
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 		BdfDictionary oldMetadata = BdfDictionary.of(
 				new BdfEntry("transportId", "foo"),
 				new BdfEntry("version", 1),
@@ -597,9 +594,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).removeMessage(txn, localGroupUpdateId);
 			// Store the merged properties in each contact's group, version 2
 			oneOf(db).getContacts(txn);
-			will(returnValue(Collections.singletonList(contact)));
+			will(returnValue(singletonList(contact)));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
 					contactGroup.getId());
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyValidatorTest.java b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyValidatorTest.java
index cd8424897f69431fa886d4109f62765a592058f9..717e5370d0f5dbfb8bcb2c6530695624b39d2588 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyValidatorTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyValidatorTest.java
@@ -18,7 +18,8 @@ import org.junit.Test;
 import java.io.IOException;
 
 import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getClientId;
+import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
+import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION;
 import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getMessage;
 import static org.briarproject.bramble.test.TestUtils.getTransportId;
@@ -42,7 +43,7 @@ public class TransportPropertyValidatorTest extends BrambleMockTestCase {
 		transportProperties = new TransportProperties();
 		transportProperties.put("foo", "bar");
 
-		group = getGroup(getClientId());
+		group = getGroup(CLIENT_ID, MAJOR_VERSION);
 		message = getMessage(group.getId());
 
 		MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
index 593a8ef71eeb034799dcd22d80a2e53e145ebf9a..1ceedbb20e3102ca52db0812ca866546acd44bce 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
@@ -81,9 +81,9 @@ public class SyncIntegrationTest extends BrambleTestCase {
 		streamNumber = 123;
 		// Create a group
 		ClientId clientId = getClientId();
-		int clientVersion = 1234567890;
+		int majorVersion = 1234567890;
 		byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
-		Group group = groupFactory.createGroup(clientId, clientVersion,
+		Group group = groupFactory.createGroup(clientId, majorVersion,
 				descriptor);
 		// Add two messages to the group
 		long timestamp = System.currentTimeMillis();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/ValidationManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/ValidationManagerImplTest.java
index a3a23d4aef7a613b74f28f98ef72c6da1bd8ef46..e3d7520e5ea8c67c277fe6ba6d1efe6f2c49373b 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/ValidationManagerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/ValidationManagerImplTest.java
@@ -53,10 +53,11 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
 	private final Executor dbExecutor = new ImmediateExecutor();
 	private final Executor validationExecutor = new ImmediateExecutor();
 	private final ClientId clientId = getClientId();
+	private final int majorVersion = 123;
 	private final MessageId messageId = new MessageId(getRandomId());
 	private final MessageId messageId1 = new MessageId(getRandomId());
 	private final MessageId messageId2 = new MessageId(getRandomId());
-	private final Group group = getGroup(clientId);
+	private final Group group = getGroup(clientId, majorVersion);
 	private final GroupId groupId = group.getId();
 	private final long timestamp = System.currentTimeMillis();
 	private final byte[] raw = new byte[123];
@@ -85,8 +86,8 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
 	public void setUp() {
 		vm = new ValidationManagerImpl(db, dbExecutor, validationExecutor,
 				messageFactory);
-		vm.registerMessageValidator(clientId, validator);
-		vm.registerIncomingMessageHook(clientId, hook);
+		vm.registerMessageValidator(clientId, majorVersion, validator);
+		vm.registerIncomingMessageHook(clientId, majorVersion, hook);
 	}
 
 	@Test
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestCryptoExecutorModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestCryptoExecutorModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fabc08d5a7086efc96bd9aca703532b57b75272
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestCryptoExecutorModule.java
@@ -0,0 +1,21 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.bramble.api.crypto.CryptoExecutor;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class TestCryptoExecutorModule {
+
+	@Provides
+	@Singleton
+	@CryptoExecutor
+	Executor provideCryptoExecutor() {
+		return new ImmediateExecutor();
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java b/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java
index 66a4a3b5630f14dd5b8f6448b320ebcd67894be8..9958573de8d1fd2565df261deda486111aaa4dde 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java
@@ -24,7 +24,7 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase {
 			context.mock(MetadataEncoder.class);
 	protected final Clock clock = context.mock(Clock.class);
 
-	protected final Group group = getGroup(getClientId());
+	protected final Group group = getGroup(getClientId(), 123);
 	protected final GroupId groupId = group.getId();
 	protected final byte[] descriptor = group.getDescriptor();
 	protected final Message message = getMessage(groupId);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7494873d9c724fea0a516c96d358bf6902ce90cf
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java
@@ -0,0 +1,669 @@
+package org.briarproject.bramble.versioning;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.jmock.Expectations;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.CLIENT_ID;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.MAJOR_VERSION;
+import static org.briarproject.bramble.test.TestUtils.getAuthor;
+import static org.briarproject.bramble.test.TestUtils.getClientId;
+import static org.briarproject.bramble.test.TestUtils.getGroup;
+import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
+import static org.briarproject.bramble.test.TestUtils.getMessage;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
+import static org.junit.Assert.assertFalse;
+
+public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
+
+	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
+	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final ContactGroupFactory contactGroupFactory =
+			context.mock(ContactGroupFactory.class);
+	private final Clock clock = context.mock(Clock.class);
+	private final ClientVersioningHook hook =
+			context.mock(ClientVersioningHook.class);
+
+	private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Contact contact = new Contact(new ContactId(123),
+			getAuthor(), getLocalAuthor().getId(), true, true);
+	private final ClientId clientId = getClientId();
+	private final long now = System.currentTimeMillis();
+	private final Transaction txn = new Transaction(null, false);
+
+	private ClientVersioningManagerImpl createInstance() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
+					MAJOR_VERSION);
+			will(returnValue(localGroup));
+		}});
+		return new ClientVersioningManagerImpl(db, clientHelper,
+				contactGroupFactory, clock);
+	}
+
+	@Test
+	public void testCreatesGroupsAtStartup() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(db).containsGroup(txn, localGroup.getId());
+			will(returnValue(false));
+			oneOf(db).addGroup(txn, localGroup);
+			oneOf(db).getContacts(txn);
+			will(returnValue(singletonList(contact)));
+		}});
+		expectAddingContact();
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.createLocalState(txn);
+	}
+
+	@Test
+	public void testDoesNotCreateGroupsAtStartupIfAlreadyCreated()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(db).containsGroup(txn, localGroup.getId());
+			will(returnValue(true));
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.createLocalState(txn);
+	}
+
+	@Test
+	public void testCreatesContactGroupWhenAddingContact() throws Exception {
+		expectAddingContact();
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.addingContact(txn, contact);
+	}
+
+	private void expectAddingContact() throws Exception {
+		BdfDictionary groupMeta = BdfDictionary.of(
+				new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
+		long now = System.currentTimeMillis();
+		BdfList localUpdateBody = BdfList.of(new BdfList(), 1L);
+		Message localUpdate = getMessage(contactGroup.getId());
+		BdfDictionary localUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+
+		context.checking(new Expectations() {{
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(db).setGroupVisibility(txn, contact.getId(),
+					contactGroup.getId(), SHARED);
+			oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
+					groupMeta);
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(contactGroup.getId(), now,
+					localUpdateBody);
+			will(returnValue(localUpdate));
+			oneOf(clientHelper).addLocalMessage(txn, localUpdate,
+					localUpdateMeta, true);
+		}});
+	}
+
+	@Test
+	public void testRemovesGroupWhenRemovingContact() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			oneOf(db).removeGroup(txn, contactGroup);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.removingContact(txn, contact);
+	}
+
+	@Test
+	public void testStoresClientVersionsAtFirstStartup() throws Exception {
+		BdfList localVersionsBody =
+				BdfList.of(BdfList.of(clientId.getString(), 123, 234));
+		Message localVersions = getMessage(localGroup.getId());
+		MessageId localUpdateId = new MessageId(getRandomId());
+		BdfDictionary localUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		BdfList localUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, false)), 1L);
+
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn));
+			// No client versions have been stored yet
+			oneOf(db).getMessageIds(txn, localGroup.getId());
+			will(returnValue(emptyList()));
+			// Store the client versions
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(localGroup.getId(), now,
+					localVersionsBody);
+			will(returnValue(localVersions));
+			oneOf(db).addLocalMessage(txn, localVersions, new Metadata(),
+					false);
+			// Inform contacts that client versions have changed
+			oneOf(db).getContacts(txn);
+			will(returnValue(singletonList(contact)));
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			// Find the latest local and remote updates (no remote update)
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
+			will(returnValue(localUpdateBody));
+			// Latest local update is up-to-date, no visibilities have changed
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		c.startService();
+	}
+
+	@Test
+	public void testComparesClientVersionsAtSubsequentStartup()
+			throws Exception {
+		MessageId localVersionsId = new MessageId(getRandomId());
+		BdfList localVersionsBody =
+				BdfList.of(BdfList.of(clientId.getString(), 123, 234));
+
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn));
+			// Load the old client versions
+			oneOf(db).getMessageIds(txn, localGroup.getId());
+			will(returnValue(singletonList(localVersionsId)));
+			oneOf(clientHelper).getMessageAsList(txn, localVersionsId);
+			will(returnValue(localVersionsBody));
+			// Client versions are up-to-date
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		c.startService();
+	}
+
+	@Test
+	public void testStoresClientVersionsAtSubsequentStartupIfChanged()
+			throws Exception {
+		// The client had minor version 234 in the old client versions
+		BdfList oldLocalVersionsBody =
+				BdfList.of(BdfList.of(clientId.getString(), 123, 234));
+		// The client has minor version 345 in the new client versions
+		BdfList newLocalVersionsBody =
+				BdfList.of(BdfList.of(clientId.getString(), 123, 345));
+		// The client had minor version 234 in the old local update
+		BdfList oldLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, false)), 1L);
+		// The client has minor version 345 in the new local update
+		BdfList newLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 345, false)), 2L);
+
+		MessageId oldLocalVersionsId = new MessageId(getRandomId());
+		Message newLocalVersions = getMessage(localGroup.getId());
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		Message newLocalUpdate = getMessage(contactGroup.getId());
+		BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn));
+			// Load the old client versions
+			oneOf(db).getMessageIds(txn, localGroup.getId());
+			will(returnValue(singletonList(oldLocalVersionsId)));
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalVersionsId);
+			will(returnValue(oldLocalVersionsBody));
+			// Delete the old client versions
+			oneOf(db).removeMessage(txn, oldLocalVersionsId);
+			// Store the new client versions
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(localGroup.getId(), now,
+					newLocalVersionsBody);
+			will(returnValue(newLocalVersions));
+			oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
+					false);
+			// Inform contacts that client versions have changed
+			oneOf(db).getContacts(txn);
+			will(returnValue(singletonList(contact)));
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			// Find the latest local and remote updates (no remote update)
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(singletonMap(oldLocalUpdateId,
+					oldLocalUpdateMeta)));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// Delete the latest local update
+			oneOf(db).deleteMessage(txn, oldLocalUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldLocalUpdateId);
+			// Store the new local update
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(contactGroup.getId(), now,
+					newLocalUpdateBody);
+			will(returnValue(newLocalUpdate));
+			oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
+					newLocalUpdateMeta, true);
+			// No visibilities have changed
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 345, hook);
+		c.startService();
+	}
+
+	@Test
+	public void testActivatesNewClientAtStartupIfAlreadyAdvertisedByContact()
+			throws Exception {
+		testActivatesNewClientAtStartup(false, VISIBLE);
+	}
+
+	@Test
+	public void testActivatesNewClientAtStartupIfAlreadyActivatedByContact()
+			throws Exception {
+		testActivatesNewClientAtStartup(true, SHARED);
+	}
+
+	private void testActivatesNewClientAtStartup(boolean remoteActive,
+			Visibility visibility) throws Exception {
+		// The client was missing from the old client versions
+		BdfList oldLocalVersionsBody = new BdfList();
+		// The client is included in the new client versions
+		BdfList newLocalVersionsBody =
+				BdfList.of(BdfList.of(clientId.getString(), 123, 234));
+		// The client was missing from the old local update
+		BdfList oldLocalUpdateBody = BdfList.of(new BdfList(), 1L);
+		// The client was included in the old remote update
+		BdfList oldRemoteUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 345, remoteActive)), 1L);
+		// The client is active in the new local update
+		BdfList newLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, true)), 2L);
+
+		MessageId oldLocalVersionsId = new MessageId(getRandomId());
+		Message newLocalVersions = getMessage(localGroup.getId());
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		MessageId oldRemoteUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldRemoteUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
+		messageMetadata.put(oldLocalUpdateId, oldLocalUpdateMeta);
+		messageMetadata.put(oldRemoteUpdateId, oldRemoteUpdateMeta);
+		Message newLocalUpdate = getMessage(localGroup.getId());
+		BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+
+		context.checking(new Expectations() {{
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn));
+			// Load the old client versions
+			oneOf(db).getMessageIds(txn, localGroup.getId());
+			will(returnValue(singletonList(oldLocalVersionsId)));
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalVersionsId);
+			will(returnValue(oldLocalVersionsBody));
+			// Delete the old client versions
+			oneOf(db).removeMessage(txn, oldLocalVersionsId);
+			// Store the new client versions
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(localGroup.getId(), now,
+					newLocalVersionsBody);
+			will(returnValue(newLocalVersions));
+			oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
+					false);
+			// Inform contacts that client versions have changed
+			oneOf(db).getContacts(txn);
+			will(returnValue(singletonList(contact)));
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, contact);
+			will(returnValue(contactGroup));
+			// Find the latest local and remote updates
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(messageMetadata));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// Load the latest remote update
+			oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
+			will(returnValue(oldRemoteUpdateBody));
+			// Delete the latest local update
+			oneOf(db).deleteMessage(txn, oldLocalUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldLocalUpdateId);
+			// Store the new local update
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(contactGroup.getId(), now,
+					newLocalUpdateBody);
+			will(returnValue(newLocalUpdate));
+			oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
+					newLocalUpdateMeta, true);
+			// The client's visibility has changed
+			oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
+			oneOf(db).commitTransaction(txn);
+			oneOf(db).endTransaction(txn);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		c.startService();
+	}
+
+	@Test
+	public void testDeletesObsoleteRemoteUpdate() throws Exception {
+		Message newRemoteUpdate = getMessage(contactGroup.getId());
+		BdfList newRemoteUpdateBody = BdfList.of(new BdfList(), 1L);
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		MessageId oldRemoteUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldRemoteUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
+		messageMetadata.put(oldLocalUpdateId, oldLocalUpdateMeta);
+		messageMetadata.put(oldRemoteUpdateId, oldRemoteUpdateMeta);
+
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).toList(newRemoteUpdate);
+			will(returnValue(newRemoteUpdateBody));
+			// Find the latest local and remote updates
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(messageMetadata));
+			// Delete the new remote update, which is obsolete
+			oneOf(db).deleteMessage(txn, newRemoteUpdate.getId());
+			oneOf(db).deleteMessageMetadata(txn, newRemoteUpdate.getId());
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
+	}
+
+	@Test
+	public void testDeletesPreviousRemoteUpdate() throws Exception {
+		Message newRemoteUpdate = getMessage(contactGroup.getId());
+		BdfList newRemoteUpdateBody = BdfList.of(new BdfList(), 2L);
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		MessageId oldRemoteUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldRemoteUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
+		messageMetadata.put(oldLocalUpdateId, oldLocalUpdateMeta);
+		messageMetadata.put(oldRemoteUpdateId, oldRemoteUpdateMeta);
+		BdfList oldLocalUpdateBody = BdfList.of(new BdfList(), 1L);
+		BdfList oldRemoteUpdateBody = BdfList.of(new BdfList(), 1L);
+
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).toList(newRemoteUpdate);
+			will(returnValue(newRemoteUpdateBody));
+			// Find the latest local and remote updates
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(messageMetadata));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// Load the latest remote update
+			oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
+			will(returnValue(oldRemoteUpdateBody));
+			// Delete the old remote update
+			oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
+			// No states or visibilities have changed
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
+	}
+
+	@Test
+	public void testAcceptsFirstRemoteUpdate() throws Exception {
+		Message newRemoteUpdate = getMessage(contactGroup.getId());
+		BdfList newRemoteUpdateBody = BdfList.of(new BdfList(), 1L);
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		BdfList oldLocalUpdateBody = BdfList.of(new BdfList(), 1L);
+
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).toList(newRemoteUpdate);
+			will(returnValue(newRemoteUpdateBody));
+			// Find the latest local and remote updates (no remote update)
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(singletonMap(oldLocalUpdateId,
+					oldLocalUpdateMeta)));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// No states or visibilities have changed
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
+	}
+
+	@Test
+	public void testActivatesClientOnIncomingMessageWhenAdvertisedByContact()
+			throws Exception {
+		testActivatesClientOnIncomingMessage(false, VISIBLE);
+	}
+
+	@Test
+	public void testActivatesClientOnIncomingMessageWhenActivatedByContact()
+			throws Exception {
+		testActivatesClientOnIncomingMessage(true, SHARED);
+	}
+
+	private void testActivatesClientOnIncomingMessage(boolean remoteActive,
+			Visibility visibility) throws Exception {
+		// The client was missing from the old remote update
+		BdfList oldRemoteUpdateBody = BdfList.of(new BdfList(), 1L);
+		// The client was inactive in the old local update
+		BdfList oldLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, false)), 1L);
+		// The client is included in the new remote update
+		BdfList newRemoteUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, remoteActive)), 2L);
+		// The client is active in the new local update
+		BdfList newLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, true)), 2L);
+
+		Message newRemoteUpdate = getMessage(contactGroup.getId());
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		MessageId oldRemoteUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldRemoteUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
+		messageMetadata.put(oldLocalUpdateId, oldLocalUpdateMeta);
+		messageMetadata.put(oldRemoteUpdateId, oldRemoteUpdateMeta);
+		Message newLocalUpdate = getMessage(contactGroup.getId());
+		BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		BdfDictionary groupMeta = BdfDictionary.of(
+				new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
+
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).toList(newRemoteUpdate);
+			will(returnValue(newRemoteUpdateBody));
+			// Find the latest local and remote updates
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(messageMetadata));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// Load the latest remote update
+			oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
+			will(returnValue(oldRemoteUpdateBody));
+			// Delete the old remote update
+			oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
+			// Delete the old local update
+			oneOf(db).deleteMessage(txn, oldLocalUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldLocalUpdateId);
+			// Store the new local update
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(contactGroup.getId(), now,
+					newLocalUpdateBody);
+			will(returnValue(newLocalUpdate));
+			oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
+					newLocalUpdateMeta, true);
+			// The client's visibility has changed
+			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(groupMeta));
+			oneOf(db).getContact(txn, contact.getId());
+			will(returnValue(contact));
+			oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
+	}
+
+	@Test
+	public void testDeactivatesClientOnIncomingMessage() throws Exception {
+		// The client was active in the old local and remote updates
+		BdfList oldLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, true)), 1L);
+		BdfList oldRemoteUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, true)), 1L);
+		// The client is missing from the new remote update
+		BdfList newRemoteUpdateBody = BdfList.of(new BdfList(), 2L);
+		// The client is inactive in the new local update
+		BdfList newLocalUpdateBody = BdfList.of(BdfList.of(
+				BdfList.of(clientId.getString(), 123, 234, false)), 2L);
+
+		Message newRemoteUpdate = getMessage(contactGroup.getId());
+		MessageId oldLocalUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		MessageId oldRemoteUpdateId = new MessageId(getRandomId());
+		BdfDictionary oldRemoteUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
+		messageMetadata.put(oldLocalUpdateId, oldLocalUpdateMeta);
+		messageMetadata.put(oldRemoteUpdateId, oldRemoteUpdateMeta);
+		Message newLocalUpdate = getMessage(contactGroup.getId());
+		BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
+				new BdfEntry(MSG_KEY_LOCAL, true));
+		BdfDictionary groupMeta = BdfDictionary.of(
+				new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
+
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).toList(newRemoteUpdate);
+			will(returnValue(newRemoteUpdateBody));
+			// Find the latest local and remote updates
+			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(messageMetadata));
+			// Load the latest local update
+			oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
+			will(returnValue(oldLocalUpdateBody));
+			// Load the latest remote update
+			oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
+			will(returnValue(oldRemoteUpdateBody));
+			// Delete the old remote update
+			oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
+			// Delete the old local update
+			oneOf(db).deleteMessage(txn, oldLocalUpdateId);
+			oneOf(db).deleteMessageMetadata(txn, oldLocalUpdateId);
+			// Store the new local update
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(now));
+			oneOf(clientHelper).createMessage(contactGroup.getId(), now,
+					newLocalUpdateBody);
+			will(returnValue(newLocalUpdate));
+			oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
+					newLocalUpdateMeta, true);
+			// The client's visibility has changed
+			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
+					contactGroup.getId());
+			will(returnValue(groupMeta));
+			oneOf(db).getContact(txn, contact.getId());
+			will(returnValue(contact));
+			oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
+		}});
+
+		ClientVersioningManagerImpl c = createInstance();
+		c.registerClient(clientId, 123, 234, hook);
+		assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningValidatorTest.java b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d13be23405cf1b1e5b2e904e9d52bd64a0adee39
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningValidatorTest.java
@@ -0,0 +1,274 @@
+package org.briarproject.bramble.versioning;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.BdfMessageContext;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.CLIENT_ID;
+import static org.briarproject.bramble.api.versioning.ClientVersioningManager.MAJOR_VERSION;
+import static org.briarproject.bramble.test.TestUtils.getClientId;
+import static org.briarproject.bramble.test.TestUtils.getGroup;
+import static org.briarproject.bramble.test.TestUtils.getMessage;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.util.StringUtils.getRandomString;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
+import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
+import static org.junit.Assert.assertEquals;
+
+public class ClientVersioningValidatorTest extends BrambleMockTestCase {
+
+	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final MetadataEncoder metadataEncoder =
+			context.mock(MetadataEncoder.class);
+	private final Clock clock = context.mock(Clock.class);
+	private final ClientVersioningValidator validator =
+			new ClientVersioningValidator(clientHelper, metadataEncoder, clock);
+
+	private final Group group = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Message message = getMessage(group.getId());
+	private final ClientId clientId = getClientId();
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBody() throws Exception {
+		BdfList body = BdfList.of(new BdfList());
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBody() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), 123, null);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullStatesList() throws Exception {
+		BdfList body = BdfList.of(null, 123);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonListStatesList() throws Exception {
+		BdfList body = BdfList.of("", 123);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsEmptyStatesList() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), 123);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 123L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullUpdateVersion() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), null);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonLongUpdateVersion() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), "123");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNegativeUpdateVersion() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), -1);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsZeroUpdateVersion() throws Exception {
+		BdfList body = BdfList.of(new BdfList(), 0);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 0L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortClientState() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 234);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongClientState() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 234, true, null);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullClientId() throws Exception {
+		BdfList state = BdfList.of(null, 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringClientId() throws Exception {
+		byte[] id = getRandomBytes(MAX_CLIENT_ID_LENGTH);
+		BdfList state = BdfList.of(id, 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortClientId() throws Exception {
+		BdfList state = BdfList.of("", 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsMinLengthClientId() throws Exception {
+		BdfList state = BdfList.of(getRandomString(1), 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 345L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongClientId() throws Exception {
+		String id = getRandomString(MAX_CLIENT_ID_LENGTH + 1);
+		BdfList state = BdfList.of(id, 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsMaxLengthClientId() throws Exception {
+		String id = getRandomString(MAX_CLIENT_ID_LENGTH);
+		BdfList state = BdfList.of(id, 123, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 345L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullMajorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), null, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonLongMajorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), "123", 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNegativeMajorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), -1, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsZeroMajorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 0, 234, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 345L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullMinorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, null, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonLongMinorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, "234", true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNegativeMinorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, -1, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsZeroMinorVersion() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 0, true);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 345L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullActiveFlag() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 234, null);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonBooleanActiveFlag() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 234, "true");
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsNegativeActiveFlag() throws Exception {
+		BdfList state = BdfList.of(clientId.getString(), 123, 234, false);
+		BdfList body = BdfList.of(BdfList.of(state), 345);
+		BdfMessageContext context =
+				validator.validateMessage(message, group, body);
+		assertEquals(emptyList(), context.getDependencies());
+		BdfDictionary expectedMeta = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_UPDATE_VERSION, 345L),
+				new BdfEntry(MSG_KEY_LOCAL, false));
+		assertEquals(expectedMeta, context.getDictionary());
+	}
+}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java
index bbf1da0944381d3d290a682a29c7405285cd4525..5381dd4901275e88f5c718484dafc751e72dbfe5 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java
@@ -22,9 +22,14 @@ public interface BlogManager {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.blog");
 
 	/**
-	 * The current version of the blog client.
+	 * The current major version of the blog client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the blog client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Adds the given {@link Blog).}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogSharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogSharingManager.java
index e4b68bd6b42cf4576414e4f01ace74aacc66cde8..f2f04afc54939a91c15e71b3c3d18e1ec0723f6c 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogSharingManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogSharingManager.java
@@ -11,7 +11,12 @@ public interface BlogSharingManager extends SharingManager<Blog> {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.blog.sharing");
 
 	/**
-	 * The current version of the blog sharing client.
+	 * The current major version of the blog sharing client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the blog sharing client.
+	 */
+	int MINOR_VERSION = 0;
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/feed/FeedManager.java b/briar-api/src/main/java/org/briarproject/briar/api/feed/FeedManager.java
index 50f67963a5e2d0bdc9367e068df28d05ed39d92e..98adcea29e040f1c73d03ec8e7b6ad8994bf9e53 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/feed/FeedManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/feed/FeedManager.java
@@ -16,9 +16,9 @@ public interface FeedManager {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.feed");
 
 	/**
-	 * The current version of the RSS feed client.
+	 * The current major version of the RSS feed client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
 
 	/**
 	 * Adds an RSS feed as a new dedicated blog.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java
index 8eb7eb1ad3504f31b38ccae8228873250574cfe8..96f44777680116348bf6eb5fe25d891376336b56 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java
@@ -23,9 +23,14 @@ public interface ForumManager {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.forum");
 
 	/**
-	 * The current version of the forum client.
+	 * The current major version of the forum client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the forum client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Subscribes to a forum.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumSharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumSharingManager.java
index 5620e024c11f99af1886f0fe51be03ae3a58138a..29b2468d0f1c396b11c609013a97ad2a241579a0 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumSharingManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumSharingManager.java
@@ -11,7 +11,12 @@ public interface ForumSharingManager extends SharingManager<Forum> {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.forum.sharing");
 
 	/**
-	 * The current version of the forum sharing client.
+	 * The current major version of the forum sharing client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the forum sharing client.
+	 */
+	int MINOR_VERSION = 0;
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
index 9a267c5c205304087f62c4add918d73bd1613c2c..532d633539e3e33b89a386383e9d9cc79122395f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
@@ -21,15 +21,20 @@ public interface IntroductionManager extends ConversationClient {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.introduction");
 
 	/**
-	 * The current version of the introduction client.
+	 * The current major version of the introduction client.
 	 */
-	int CLIENT_VERSION = 1;
+	int MAJOR_VERSION = 1;
 
 	/**
 	 * Returns true if both contacts can be introduced at this moment.
 	 */
 	boolean canIntroduce(Contact c1, Contact c2) throws DbException;
 
+	/**
+	 * The current minor version of the introduction client.
+	 */
+	int MINOR_VERSION = 0;
+
 	/**
 	 * Sends two initial introduction messages.
 	 */
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java
index 65f406a6faa8bb9274110ee54af624136982fff4..f29d2c9e451468169d15dbcfb177fe779aac3329 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java
@@ -19,9 +19,14 @@ public interface MessagingManager extends ConversationClient {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.messaging");
 
 	/**
-	 * The current version of the messaging client.
+	 * The current major version of the messaging client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the messaging client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Stores a local private message.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java
index 7d08ae016dfd8e6be85c03eafe7cd35dbb9f6017..de4caecfc6a15d91e0eb130efbdb474800ffbfeb 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java
@@ -22,9 +22,14 @@ public interface PrivateGroupManager {
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.privategroup");
 
 	/**
-	 * The current version of the private group client.
+	 * The current major version of the private group client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the private group client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Adds a new private group and joins it.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
index fc3ca339f30ba6f9c9822dcc9944c6728e3493bf..1062d0ce1127f828425532cb56fd504ea6e89380 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationManager.java
@@ -26,9 +26,14 @@ public interface GroupInvitationManager extends ConversationClient {
 			new ClientId("org.briarproject.briar.privategroup.invitation");
 
 	/**
-	 * The current version of the private group invitation client.
+	 * The current major version of the private group invitation client.
 	 */
-	int CLIENT_VERSION = 0;
+	int MAJOR_VERSION = 0;
+
+	/**
+	 * The current minor version of the private group invitation client.
+	 */
+	int MINOR_VERSION = 0;
 
 	/**
 	 * Sends an invitation to share the given private group with the given
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogFactoryImpl.java
index aecc020f93d868d56bab87ddc3482aa7563aa7e8..ca7887f0d76707b9b7c3fde120bbd71d39a34938 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogFactoryImpl.java
@@ -15,7 +15,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.util.ValidationUtils.checkSize;
 import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
-import static org.briarproject.briar.api.blog.BlogManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
 
 @Immutable
 @NotNullByDefault
@@ -45,7 +45,7 @@ class BlogFactoryImpl implements BlogFactory {
 		try {
 			BdfList blog = BdfList.of(clientHelper.toList(a), rssFeed);
 			byte[] descriptor = clientHelper.toByteArray(blog);
-			Group g = groupFactory.createGroup(CLIENT_ID, CLIENT_VERSION,
+			Group g = groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION,
 					descriptor);
 			return new Blog(g, a, rssFeed);
 		} catch (FormatException e) {
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
index c7b119ef0c220342866992c2b5af710da0e09922..6a05b9a1542b00d463ddad795f864b97079a2d7e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java
@@ -425,7 +425,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
 			Collection<Group> groups;
 			Transaction txn = db.startTransaction(true);
 			try {
-				groups = db.getGroups(txn, CLIENT_ID);
+				groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION);
 				for (Group g : groups) {
 					blogs.add(blogFactory.parseBlog(g));
 				}
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogModule.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogModule.java
index af39a8fcd9134af40874bae4fa19b977d2aa1689..dcc829d392c01cbd817a32a658b1dc10298f91ad 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogModule.java
@@ -18,7 +18,8 @@ import javax.inject.Singleton;
 import dagger.Module;
 import dagger.Provides;
 
-import static org.briarproject.briar.blog.BlogManagerImpl.CLIENT_ID;
+import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
 
 @Module
 public class BlogModule {
@@ -35,10 +36,10 @@ public class BlogModule {
 	BlogManager provideBlogManager(BlogManagerImpl blogManager,
 			LifecycleManager lifecycleManager, ContactManager contactManager,
 			ValidationManager validationManager) {
-
 		lifecycleManager.registerClient(blogManager);
 		contactManager.registerContactHook(blogManager);
-		validationManager.registerIncomingMessageHook(CLIENT_ID, blogManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
+				blogManager);
 		return blogManager;
 	}
 
@@ -60,12 +61,11 @@ public class BlogModule {
 			MessageFactory messageFactory, BlogFactory blogFactory,
 			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
 			Clock clock) {
-
 		BlogPostValidator validator = new BlogPostValidator(groupFactory,
 				messageFactory, blogFactory, clientHelper, metadataEncoder,
 				clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
-
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogPostValidator.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogPostValidator.java
index 6dc845623967bf04289ba1f7b187bd18bf3c2132..0b53468df84f2c1efab4669b08f8c9c465531925 100644
--- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogPostValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogPostValidator.java
@@ -42,7 +42,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
 import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_COMMENT_LENGTH;
 import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_BODY_LENGTH;
 import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
-import static org.briarproject.briar.api.blog.BlogManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
 import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_COMMENT;
 import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_POST;
 import static org.briarproject.briar.api.blog.MessageType.COMMENT;
@@ -195,7 +195,7 @@ class BlogPostValidator extends BdfMessageValidator {
 		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
 
 		// Get and Validate the Wrapped Message
-		Group wGroup = groupFactory.createGroup(CLIENT_ID, CLIENT_VERSION,
+		Group wGroup = groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION,
 				descriptor);
 		Blog wBlog = blogFactory.parseBlog(wGroup);
 		BdfList wBodyList = BdfList.of(POST.getInt(), content, signature);
@@ -258,7 +258,7 @@ class BlogPostValidator extends BdfMessageValidator {
 		MessageId parentId = new MessageId(parentIdBytes);
 
 		// Get and Validate the Wrapped Comment
-		Group wGroup = groupFactory.createGroup(CLIENT_ID, CLIENT_VERSION,
+		Group wGroup = groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION,
 				descriptor);
 		BdfList wBodyList = BdfList.of(COMMENT.getInt(), comment, pOriginalId,
 				oldId, signature);
diff --git a/briar-core/src/main/java/org/briarproject/briar/feed/FeedManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/feed/FeedManagerImpl.java
index f4d94413d9634fd6861b5799a77e849682c55d99..a4d58d3614a07c8486318e876a176303836b7b37 100644
--- a/briar-core/src/main/java/org/briarproject/briar/feed/FeedManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/feed/FeedManagerImpl.java
@@ -496,7 +496,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener,
 	}
 
 	private Group getLocalGroup() {
-		return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
+		return contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
 	}
 
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumFactoryImpl.java
index 4588db4c8e185edce9365403e38879f4e24b4414..e49acc5eef62ac7809b2d9a6c1589feb0bdedadf 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumFactoryImpl.java
@@ -18,7 +18,7 @@ import javax.inject.Inject;
 import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
 import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
-import static org.briarproject.briar.api.forum.ForumManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.forum.ForumManager.MAJOR_VERSION;
 
 @Immutable
 @NotNullByDefault
@@ -52,7 +52,7 @@ class ForumFactoryImpl implements ForumFactory {
 		try {
 			BdfList forum = BdfList.of(name, salt);
 			byte[] descriptor = clientHelper.toByteArray(forum);
-			Group g = groupFactory.createGroup(CLIENT_ID, CLIENT_VERSION,
+			Group g = groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION,
 					descriptor);
 			return new Forum(g, name, salt);
 		} catch (FormatException e) {
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
index cdd2ba064bfd135673ef33014d5dbab4dfb42d3f..3a386420801b0a7645b7cb5d81d9e717bf7cae95 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
@@ -188,7 +188,7 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 			Collection<Group> groups;
 			Transaction txn = db.startTransaction(true);
 			try {
-				groups = db.getGroups(txn, CLIENT_ID);
+				groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION);
 				db.commitTransaction(txn);
 			} finally {
 				db.endTransaction(txn);
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumModule.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumModule.java
index 9050dc272af580613adc29c430885636b33b60cb..fd1703232b06c5f933683a17895aef72aaa7bc12 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumModule.java
@@ -15,6 +15,7 @@ import dagger.Module;
 import dagger.Provides;
 
 import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
+import static org.briarproject.briar.api.forum.ForumManager.MAJOR_VERSION;
 
 @Module
 public class ForumModule {
@@ -30,7 +31,7 @@ public class ForumModule {
 	@Singleton
 	ForumManager provideForumManager(ForumManagerImpl forumManager,
 			ValidationManager validationManager) {
-		validationManager.registerIncomingMessageHook(CLIENT_ID,
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
 				forumManager);
 		return forumManager;
 	}
@@ -53,7 +54,8 @@ public class ForumModule {
 			MetadataEncoder metadataEncoder, Clock clock) {
 		ForumPostValidator validator = new ForumPostValidator(clientHelper,
 				metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
index f9d53323b08aeb90b6f81ed46018051ce6418c6e..d892c58b3cac4376c0586b0b8f6f4facb6c844c4 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
@@ -29,7 +29,7 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.LABE
 import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_BOB_MAC_KEY;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_MASTER_KEY;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
 import static org.briarproject.briar.introduction.IntroduceeSession.Local;
 
 @Immutable
@@ -94,7 +94,7 @@ class IntroductionCryptoImpl implements IntroductionCrypto {
 				LABEL_MASTER_KEY,
 				remoteEphemeralPublicKey,
 				keyPair,
-				new byte[] {CLIENT_VERSION},
+				new byte[] {MAJOR_VERSION},
 				alice ? publicKey : remotePublicKey,
 				alice ? remotePublicKey : publicKey
 		);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index efb857a0f57699ae41d9bacd541656085fc80eb7..fd39a865fefbb6861a7d29a65c17a7cd1c116004 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -20,10 +20,13 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.MessageStatus;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionManager;
@@ -44,7 +47,6 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
 import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 import static org.briarproject.briar.introduction.IntroducerState.START;
@@ -59,8 +61,10 @@ import static org.briarproject.briar.introduction.MessageType.REQUEST;
 @Immutable
 @NotNullByDefault
 class IntroductionManagerImpl extends ConversationClientImpl
-		implements IntroductionManager, Client, ContactHook {
+		implements IntroductionManager, Client, ContactHook,
+		ClientVersioningHook {
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final ContactGroupFactory contactGroupFactory;
 	private final ContactManager contactManager;
 	private final MessageParser messageParser;
@@ -77,6 +81,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 	IntroductionManagerImpl(
 			DatabaseComponent db,
 			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser,
 			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
@@ -89,6 +94,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 			IntroductionCrypto crypto,
 			IdentityManager identityManager) {
 		super(db, clientHelper, metadataParser, messageTracker);
+		this.clientVersioningManager = clientVersioningManager;
 		this.contactGroupFactory = contactGroupFactory;
 		this.contactManager = contactManager;
 		this.messageParser = messageParser;
@@ -99,7 +105,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 		this.crypto = crypto;
 		this.identityManager = identityManager;
 		this.localGroup =
-				contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
+				contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
 	}
 
 	@Override
@@ -112,13 +118,14 @@ class IntroductionManagerImpl extends ConversationClientImpl
 	}
 
 	@Override
-	// TODO adapt to use upcoming ClientVersioning client
 	public void addingContact(Transaction txn, Contact c) throws DbException {
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
-		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		// Apply the client's visibility to the contact group
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
 		// Attach the contact ID to the group
 		BdfDictionary meta = new BdfDictionary();
 		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
@@ -138,10 +145,18 @@ class IntroductionManagerImpl extends ConversationClientImpl
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
 	@Override
 	public Group getContactGroup(Contact c) {
 		return contactGroupFactory
-				.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
+				.createContactGroup(CLIENT_ID, MAJOR_VERSION, c);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
index 24c649c5fd6cbb0e4a1fe7ad838af2ff13ac4d20..932de34083f318ad926995870a5d6d7f3e543a42 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.introduction.IntroductionManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
 
@@ -16,6 +17,8 @@ import dagger.Module;
 import dagger.Provides;
 
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MINOR_VERSION;
 
 @Module
 public class IntroductionModule {
@@ -32,13 +35,11 @@ public class IntroductionModule {
 	IntroductionValidator provideValidator(ValidationManager validationManager,
 			MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
 			ClientHelper clientHelper, Clock clock) {
-
 		IntroductionValidator introductionValidator =
 				new IntroductionValidator(messageEncoder, clientHelper,
 						metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID,
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
 				introductionValidator);
-
 		return introductionValidator;
 	}
 
@@ -48,13 +49,15 @@ public class IntroductionModule {
 			LifecycleManager lifecycleManager, ContactManager contactManager,
 			ValidationManager validationManager,
 			ConversationManager conversationManager,
+			ClientVersioningManager clientVersioningManager,
 			IntroductionManagerImpl introductionManager) {
 		lifecycleManager.registerClient(introductionManager);
 		contactManager.registerContactHook(introductionManager);
 		validationManager.registerIncomingMessageHook(CLIENT_ID,
-				introductionManager);
+				MAJOR_VERSION, introductionManager);
 		conversationManager.registerConversationClient(introductionManager);
-
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, introductionManager);
 		return introductionManager;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
index c81766492f98ea749ca83b4d38a67f0105fc032b..555a209905a810fd57df4dd4df78691a427d170b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
@@ -15,10 +15,13 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.MessageStatus;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.briarproject.briar.api.messaging.PrivateMessage;
@@ -33,21 +36,23 @@ import java.util.Map;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
 
 @Immutable
 @NotNullByDefault
 class MessagingManagerImpl extends ConversationClientImpl
-		implements MessagingManager, Client, ContactHook {
+		implements MessagingManager, Client, ContactHook, ClientVersioningHook {
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final ContactGroupFactory contactGroupFactory;
 
 	@Inject
 	MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser, MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory) {
 		super(db, clientHelper, metadataParser, messageTracker);
+		this.clientVersioningManager = clientVersioningManager;
 		this.contactGroupFactory = contactGroupFactory;
 	}
 
@@ -55,36 +60,36 @@ class MessagingManagerImpl extends ConversationClientImpl
 	public void createLocalState(Transaction txn) throws DbException {
 		// Create a local group to indicate that we've set this client up
 		Group localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
-				CLIENT_VERSION);
+				MAJOR_VERSION);
 		if (db.containsGroup(txn, localGroup.getId())) return;
 		db.addGroup(txn, localGroup);
-		// Ensure we've set things up for any pre-existing contacts
+		// Set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		db.addGroup(txn, g);
+		// Apply the client's visibility to the contact group
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
+		// Attach the contact ID to the group
+		BdfDictionary d = new BdfDictionary();
+		d.put("contactId", c.getId().getInt());
 		try {
-			// Create a group to share with the contact
-			Group g = getContactGroup(c);
-			// Return if we've already set things up for this contact
-			if (db.containsGroup(txn, g.getId())) return;
-			// Store the group and share it with the contact
-			db.addGroup(txn, g);
-			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
-			// Attach the contact ID to the group
-			BdfDictionary d = new BdfDictionary();
-			d.put("contactId", c.getId().getInt());
 			clientHelper.mergeGroupMetadata(txn, g.getId(), d);
 		} catch (FormatException e) {
-			throw new RuntimeException(e);
+			throw new AssertionError(e);
 		}
 	}
 
 	@Override
 	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, c);
+				MAJOR_VERSION, c);
 	}
 
 	@Override
@@ -92,6 +97,14 @@ class MessagingManagerImpl extends ConversationClientImpl
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
 	@Override
 	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
 			BdfDictionary meta) throws DbException, FormatException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
index 492d3c4fac7e4c9ddde40f8843a5ae79054d26d7..016f1225a2f554b6d0879cd7bab05f74372f3c6c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.briarproject.briar.api.messaging.PrivateMessageFactory;
@@ -16,7 +17,9 @@ import javax.inject.Singleton;
 import dagger.Module;
 import dagger.Provides;
 
-import static org.briarproject.briar.messaging.MessagingManagerImpl.CLIENT_ID;
+import static org.briarproject.briar.api.messaging.MessagingManager.CLIENT_ID;
+import static org.briarproject.briar.api.messaging.MessagingManager.MAJOR_VERSION;
+import static org.briarproject.briar.api.messaging.MessagingManager.MINOR_VERSION;
 
 @Module
 public class MessagingModule {
@@ -43,7 +46,8 @@ public class MessagingModule {
 			Clock clock) {
 		PrivateMessageValidator validator = new PrivateMessageValidator(
 				clientHelper, metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
@@ -52,12 +56,15 @@ public class MessagingModule {
 	MessagingManager getMessagingManager(LifecycleManager lifecycleManager,
 			ContactManager contactManager, ValidationManager validationManager,
 			ConversationManager conversationManager,
+			ClientVersioningManager clientVersioningManager,
 			MessagingManagerImpl messagingManager) {
 		lifecycleManager.registerClient(messagingManager);
 		contactManager.registerContactHook(messagingManager);
-		validationManager
-				.registerIncomingMessageHook(CLIENT_ID, messagingManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
+				messagingManager);
 		conversationManager.registerConversationClient(messagingManager);
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, messagingManager);
 		return messagingManager;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupFactoryImpl.java
index 4cff80dd94e03c0ddb64071712760fb8f683a485..864ad23aba1d802980e3b8f09b29758a3fe74608 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupFactoryImpl.java
@@ -21,7 +21,7 @@ import static org.briarproject.bramble.util.ValidationUtils.checkSize;
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
 import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
-import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.MAJOR_VERSION;
 
 @Immutable
 @NotNullByDefault
@@ -57,7 +57,7 @@ class PrivateGroupFactoryImpl implements PrivateGroupFactory {
 			BdfList creatorList = clientHelper.toList(creator);
 			BdfList group = BdfList.of(creatorList, name, salt);
 			byte[] descriptor = clientHelper.toByteArray(group);
-			Group g = groupFactory.createGroup(CLIENT_ID, CLIENT_VERSION,
+			Group g = groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION,
 					descriptor);
 			return new PrivateGroup(g, name, creator, salt);
 		} catch (FormatException e) {
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
index 20ea827a31167b90a2dfb858c7c00a99adc1079d..531282fdf000f129f642a93126b4106b7238fdc6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
@@ -271,7 +271,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
 		Collection<Group> groups;
 		Transaction txn = db.startTransaction(true);
 		try {
-			groups = db.getGroups(txn, CLIENT_ID);
+			groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION);
 			db.commitTransaction(txn);
 		} finally {
 			db.endTransaction(txn);
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupModule.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupModule.java
index 8b2bb3b66e69d034978db9cb0d4e85f3cbb60712..61901bc8bc101ae6d3ce8e9cac2d6d37e3e250eb 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupModule.java
@@ -16,6 +16,7 @@ import dagger.Module;
 import dagger.Provides;
 
 import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.MAJOR_VERSION;
 
 @Module
 public class PrivateGroupModule {
@@ -32,7 +33,8 @@ public class PrivateGroupModule {
 	PrivateGroupManager provideGroupManager(
 			PrivateGroupManagerImpl groupManager,
 			ValidationManager validationManager) {
-		validationManager.registerIncomingMessageHook(CLIENT_ID, groupManager);
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
+				groupManager);
 		return groupManager;
 	}
 
@@ -58,7 +60,8 @@ public class PrivateGroupModule {
 		GroupMessageValidator validator = new GroupMessageValidator(
 				privateGroupFactory, clientHelper, metadataEncoder, clock,
 				groupInvitationFactory);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
index c68f7b662513be4804cfbabc8eee086ad6b6205d..d3bd73943c5ae155c905d1dc6c1e69479b9eaa39 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
@@ -16,6 +16,7 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.privategroup.GroupMessage;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -28,6 +29,8 @@ import java.util.Map;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.MAJOR_VERSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
 import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
@@ -45,6 +48,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 	protected final PrivateGroupFactory privateGroupFactory;
 	protected final MessageTracker messageTracker;
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final GroupMessageFactory groupMessageFactory;
 	private final IdentityManager identityManager;
 	private final MessageParser messageParser;
@@ -52,6 +56,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 	private final Clock clock;
 
 	AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
@@ -60,6 +65,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 			Clock clock) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.privateGroupManager = privateGroupManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.groupMessageFactory = groupMessageFactory;
@@ -90,10 +96,14 @@ abstract class AbstractProtocolEngine<S extends Session>
 		return expected != null && dependency.equals(expected);
 	}
 
-	void setPrivateGroupVisibility(Transaction txn, S session, Visibility v)
-			throws DbException, FormatException {
+	void setPrivateGroupVisibility(Transaction txn, S session,
+			Visibility preferred) throws DbException, FormatException {
+		// Apply min of preferred visibility and client's visibility
 		ContactId contactId = getContactId(txn, session.getContactGroupId());
-		db.setGroupVisibility(txn, contactId, session.getPrivateGroupId(), v);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				contactId, CLIENT_ID, MAJOR_VERSION);
+		Visibility min = Visibility.min(preferred, client);
+		db.setGroupVisibility(txn, contactId, session.getPrivateGroupId(), min);
 	}
 
 	Message sendInviteMessage(Transaction txn, S session,
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
index ec5315a6d07745b2552156a99905b306889d6c43..d043fbad260f7e00ce42c7d2172d68773b6ba54f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.client.SessionId;
@@ -36,15 +37,16 @@ import static org.briarproject.briar.privategroup.invitation.CreatorState.START;
 class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 
 	CreatorProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
index 4afe89c4d8319b410f1ab4be1b945446e2db30a0..6fe69a08c9227bf9e9433813aa9c354a6fa2a70b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
@@ -2,19 +2,30 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+
 @Immutable
 @NotNullByDefault
 enum CreatorState implements State {
 
-	START(0), INVITED(1), JOINED(2), LEFT(3), DISSOLVED(4), ERROR(5);
+	START(0, INVISIBLE),
+	INVITED(1, INVISIBLE),
+	JOINED(2, SHARED),
+	LEFT(3, INVISIBLE),
+	DISSOLVED(4, INVISIBLE),
+	ERROR(5, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	CreatorState(int value) {
+	CreatorState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -22,6 +33,11 @@ enum CreatorState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static CreatorState fromValue(int value) throws FormatException {
 		for (CreatorState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
index a9a6ef0929eadae2009d368e7bb51c8c9ecfff75..90c0fcd29935080e3bd6ed89405ae7ce74cd95b2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
@@ -15,6 +15,7 @@ interface GroupInvitationConstants {
 	String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted";
 
 	// Session keys
+	String SESSION_KEY_IS_SESSION = "isSession";
 	String SESSION_KEY_SESSION_ID = "sessionId";
 	String SESSION_KEY_PRIVATE_GROUP_ID = "privateGroupId";
 	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationFactoryImpl.java
index 29ca211f6debe399d920fe954aebc30be956fef1..c8fc2b4ba5c3a6541eeda14286f659393591166e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationFactoryImpl.java
@@ -17,7 +17,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
-import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
 
 @Immutable
 @NotNullByDefault
@@ -53,7 +53,7 @@ class GroupInvitationFactoryImpl implements GroupInvitationFactory {
 	public BdfList createInviteToken(AuthorId creatorId, AuthorId memberId,
 			GroupId privateGroupId, long timestamp) {
 		Group contactGroup = contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, creatorId, memberId);
+				MAJOR_VERSION, creatorId, memberId);
 		return BdfList.of(
 				timestamp,
 				contactGroup.getId(),
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
index 3c02de9f4653064f98fbb943b104bb961b21ef9d..a4aef1eca325f79485c255c311510cd9d58219d4 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -17,10 +17,13 @@ import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.MessageStatus;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
@@ -36,6 +39,7 @@ import org.briarproject.briar.client.ConversationClientImpl;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -59,8 +63,9 @@ import static org.briarproject.briar.privategroup.invitation.Role.PEER;
 @NotNullByDefault
 class GroupInvitationManagerImpl extends ConversationClientImpl
 		implements GroupInvitationManager, Client, ContactHook,
-		PrivateGroupHook {
+		PrivateGroupHook, ClientVersioningHook {
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final ContactGroupFactory contactGroupFactory;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final PrivateGroupManager privateGroupManager;
@@ -73,8 +78,9 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 
 	@Inject
 	GroupInvitationManagerImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MetadataParser metadataParser,
-			MessageTracker messageTracker,
+			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser, MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
 			PrivateGroupFactory privateGroupFactory,
 			PrivateGroupManager privateGroupManager,
@@ -82,6 +88,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			SessionEncoder sessionEncoder,
 			ProtocolEngineFactory engineFactory) {
 		super(db, clientHelper, metadataParser, messageTracker);
+		this.clientVersioningManager = clientVersioningManager;
 		this.contactGroupFactory = contactGroupFactory;
 		this.privateGroupFactory = privateGroupFactory;
 		this.privateGroupManager = privateGroupManager;
@@ -97,10 +104,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	public void createLocalState(Transaction txn) throws DbException {
 		// Create a local group to indicate that we've set this client up
 		Group localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
-				CLIENT_VERSION);
+				MAJOR_VERSION);
 		if (db.containsGroup(txn, localGroup.getId())) return;
 		db.addGroup(txn, localGroup);
-		// Ensure we've set things up for any pre-existing contacts
+		// Set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
@@ -108,11 +115,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	public void addingContact(Transaction txn, Contact c) throws DbException {
 		// Create a group to share with the contact
 		Group g = getContactGroup(c);
-		// Return if we've already set things up for this contact
-		if (db.containsGroup(txn, g.getId())) return;
-		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), CLIENT_ID, MAJOR_VERSION);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
 		// Attach the contact ID to the group
 		BdfDictionary meta = new BdfDictionary();
 		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
@@ -122,7 +128,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			throw new AssertionError(e);
 		}
 		// If the contact belongs to any private groups, create a peer session
-		for (Group pg : db.getGroups(txn, PrivateGroupManager.CLIENT_ID)) {
+		for (Group pg : db.getGroups(txn, PrivateGroupManager.CLIENT_ID,
+				PrivateGroupManager.MAJOR_VERSION)) {
 			if (privateGroupManager.isMember(txn, pg.getId(), c.getAuthor()))
 				addingMember(txn, pg.getId(), c);
 		}
@@ -137,7 +144,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	@Override
 	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, c);
+				MAJOR_VERSION, c);
 	}
 
 	@Override
@@ -461,8 +468,13 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		SessionId sessionId = getSessionId(privateGroupId);
 		Transaction txn = db.startTransaction(true);
 		try {
+			Visibility client = clientVersioningManager.getClientVisibility(txn,
+					c.getId(), PrivateGroupManager.CLIENT_ID,
+					PrivateGroupManager.MAJOR_VERSION);
 			StoredSession ss = getSession(txn, contactGroupId, sessionId);
 			db.commitTransaction(txn);
+			// The group can't be shared unless the contact supports the client
+			if (client != SHARED) return false;
 			// If there's no session, the contact can be invited
 			if (ss == null) return true;
 			// If the session's in the start state, the contact can be invited
@@ -566,6 +578,65 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		}
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
+	ClientVersioningHook getPrivateGroupClientVersioningHook() {
+		return this::onPrivateGroupClientVisibilityChanging;
+	}
+
+	private void onPrivateGroupClientVisibilityChanging(Transaction txn,
+			Contact c, Visibility client) throws DbException {
+		try {
+			Collection<Group> shareables =
+					db.getGroups(txn, PrivateGroupManager.CLIENT_ID,
+							PrivateGroupManager.MAJOR_VERSION);
+			Map<GroupId, Visibility> m = getPreferredVisibilities(txn, c);
+			for (Group g : shareables) {
+				Visibility preferred = m.get(g.getId());
+				if (preferred == null) continue; // No session for this group
+				// Apply min of preferred visibility and client's visibility
+				Visibility min = Visibility.min(preferred, client);
+				db.setGroupVisibility(txn, c.getId(), g.getId(), min);
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private Map<GroupId, Visibility> getPreferredVisibilities(Transaction txn,
+			Contact c) throws DbException, FormatException {
+		GroupId contactGroupId = getContactGroup(c).getId();
+		BdfDictionary query = sessionParser.getAllSessionsQuery();
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+		Map<GroupId, Visibility> m = new HashMap<>();
+		for (BdfDictionary d : results.values()) {
+			Role role = sessionParser.getRole(d);
+			if (role == CREATOR) {
+				CreatorSession s =
+						sessionParser.parseCreatorSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else if (role == INVITEE) {
+				InviteeSession s =
+						sessionParser.parseInviteeSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else if (role == PEER) {
+				PeerSession s =
+						sessionParser.parsePeerSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else {
+				throw new AssertionError();
+			}
+		}
+		return m;
+	}
+
 	private static class StoredSession {
 
 		private final MessageId storageId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
index fbd5efbc132ff187344f5cc07a897a60a1e07ad0..9dc9c84ad1edd564a22c4bb5a3367de2297235ce 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupManager;
@@ -19,6 +20,8 @@ import dagger.Module;
 import dagger.Provides;
 
 import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
+import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
+import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MINOR_VERSION;
 
 @Module
 public class GroupInvitationModule {
@@ -37,13 +40,22 @@ public class GroupInvitationModule {
 			LifecycleManager lifecycleManager,
 			ValidationManager validationManager, ContactManager contactManager,
 			PrivateGroupManager privateGroupManager,
-			ConversationManager conversationManager) {
+			ConversationManager conversationManager,
+			ClientVersioningManager clientVersioningManager) {
 		lifecycleManager.registerClient(groupInvitationManager);
-		validationManager.registerIncomingMessageHook(CLIENT_ID,
+		validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
 				groupInvitationManager);
 		contactManager.registerContactHook(groupInvitationManager);
 		privateGroupManager.registerPrivateGroupHook(groupInvitationManager);
 		conversationManager.registerConversationClient(groupInvitationManager);
+		clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
+				MINOR_VERSION, groupInvitationManager);
+		// The group invitation manager handles client visibility changes for
+		// the private group manager
+		clientVersioningManager.registerClient(PrivateGroupManager.CLIENT_ID,
+				PrivateGroupManager.MAJOR_VERSION,
+				PrivateGroupManager.MINOR_VERSION,
+				groupInvitationManager.getPrivateGroupClientVersioningHook());
 		return groupInvitationManager;
 	}
 
@@ -57,7 +69,8 @@ public class GroupInvitationModule {
 		GroupInvitationValidator validator = new GroupInvitationValidator(
 				clientHelper, metadataEncoder, clock, privateGroupFactory,
 				messageEncoder);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
+				validator);
 		return validator;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
index f181d1f7764a5507ad6e8725bf56902e4e02eece..a0e73d9b05d43f7a8f3ec227b03b37b6c5ef7b4c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.client.SessionId;
@@ -41,15 +42,16 @@ import static org.briarproject.briar.privategroup.invitation.InviteeState.START;
 class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 
 	InviteeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
@@ -212,6 +214,12 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 			throws DbException {
 		// Send a LEAVE message
 		Message sent = sendLeaveMessage(txn, s, false);
+		try {
+			// Make the private group invisible to the contact
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
 		// Move to the LEFT state
 		return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
index 7fa69e6af9c1c6113a70a92edf86b48d31b80dd6..f347ca06188cae611e91fa3c08700f7bdae7bdbf 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
@@ -2,20 +2,32 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+
 @Immutable
 @NotNullByDefault
 enum InviteeState implements State {
 
-	START(0), INVITED(1), ACCEPTED(2), JOINED(3), LEFT(4), DISSOLVED(5),
-	ERROR(6);
+	START(0, INVISIBLE),
+	INVITED(1, INVISIBLE),
+	ACCEPTED(2, VISIBLE),
+	JOINED(3, SHARED),
+	LEFT(4, INVISIBLE),
+	DISSOLVED(5, INVISIBLE),
+	ERROR(6, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	InviteeState(int value) {
+	InviteeState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -23,6 +35,11 @@ enum InviteeState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static InviteeState fromValue(int value) throws FormatException {
 		for (InviteeState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
index c1908735fde51d986fd5646dcc79296bae0c13d5..9b7096b63b7ac48f2eea99fb5b1522b095f46f06 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -36,15 +37,16 @@ import static org.briarproject.briar.privategroup.invitation.PeerState.START;
 class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 
 	PeerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
index 94f9d19afe624a57645bde631c98219c062761e8..041528266eb99a42dfa40fd39584b7b783e0a9cc 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
@@ -2,20 +2,32 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+
 @Immutable
 @NotNullByDefault
 enum PeerState implements State {
 
-	START(0), AWAIT_MEMBER(1), NEITHER_JOINED(2), LOCAL_JOINED(3),
-	BOTH_JOINED(4), LOCAL_LEFT(5), ERROR(6);
+	START(0, INVISIBLE),
+	AWAIT_MEMBER(1, INVISIBLE),
+	NEITHER_JOINED(2, INVISIBLE),
+	LOCAL_JOINED(3, VISIBLE),
+	BOTH_JOINED(4, SHARED),
+	LOCAL_LEFT(5, INVISIBLE),
+	ERROR(6, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	PeerState(int value) {
+	PeerState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -23,6 +35,11 @@ enum PeerState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static PeerState fromValue(int value) throws FormatException {
 		for (PeerState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
index af3bbe81673944079a40db128630757507390689..d5403102909b8f2c14b355d529bd526bcf330566 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
 import org.briarproject.briar.api.privategroup.PrivateGroupFactory;
@@ -19,6 +20,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	private final DatabaseComponent db;
 	private final ClientHelper clientHelper;
+	private final ClientVersioningManager clientVersioningManager;
 	private final PrivateGroupManager privateGroupManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final GroupMessageFactory groupMessageFactory;
@@ -30,6 +32,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	@Inject
 	ProtocolEngineFactoryImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
@@ -38,6 +41,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 			Clock clock) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.privateGroupManager = privateGroupManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.groupMessageFactory = groupMessageFactory;
@@ -50,21 +54,24 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	@Override
 	public ProtocolEngine<CreatorSession> createCreatorEngine() {
-		return new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
+		return new CreatorProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
 	public ProtocolEngine<InviteeSession> createInviteeEngine() {
-		return new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
+		return new InviteeProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
 	public ProtocolEngine<PeerSession> createPeerEngine() {
-		return new PeerProtocolEngine(db, clientHelper, privateGroupManager,
+		return new PeerProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
index 78e36bf3a5b15732e6298678cc71d10b026d003e..86fe5e3a044e52dc295e1660726812ac76d8a328 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
@@ -9,6 +9,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -28,6 +29,7 @@ class SessionEncoderImpl implements SessionEncoder {
 	@Override
 	public BdfDictionary encodeSession(Session s) {
 		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_IS_SESSION, true);
 		d.put(SESSION_KEY_SESSION_ID, s.getPrivateGroupId());
 		d.put(SESSION_KEY_PRIVATE_GROUP_ID, s.getPrivateGroupId());
 		MessageId lastLocalMessageId = s.getLastLocalMessageId();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
index baf0de80e2f392f270c1e57ce52e94d6de5dd061..152ed067e32c879d111df13e7f5c2442d6fe7e65 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
@@ -11,6 +11,8 @@ interface SessionParser {
 
 	BdfDictionary getSessionQuery(SessionId s);
 
+	BdfDictionary getAllSessionsQuery();
+
 	Role getRole(BdfDictionary d) throws FormatException;
 
 	CreatorSession parseCreatorSession(GroupId contactGroupId, BdfDictionary d)
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
index e8813b6dcd50082ea577c4229c5f6a9a1d2795fd..19a424d707e460cf8f023e42d1cb7a4dab028543 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
@@ -13,6 +13,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -37,6 +38,11 @@ class SessionParserImpl implements SessionParser {
 		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
 	}
 
+	@Override
+	public BdfDictionary getAllSessionsQuery() {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_IS_SESSION, true));
+	}
+
 	@Override
 	public Role getRole(BdfDictionary d) throws FormatException {
 		return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
index 46df85efdc5dff661db8862eb557b514fe16a6e9..2313f96c7e0cf5c3620b116ebceef9d6b18d2650 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
@@ -1,6 +1,10 @@
 package org.briarproject.briar.privategroup.invitation;
 
+import org.briarproject.bramble.api.sync.Group.Visibility;
+
 interface State {
 
 	int getValue();
+
+	Visibility getVisibility();
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogProtocolEngineImpl.java
index 3083387b6bf1f76f393b6dc6d2066c67f32e8e77..25e0d0e8e1ec57b194cd8fe5f08e615ba5a98b88 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogProtocolEngineImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogProtocolEngineImpl.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
 import org.briarproject.briar.api.blog.BlogManager;
@@ -22,6 +23,9 @@ import org.briarproject.briar.api.sharing.InvitationRequest;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
+
 @Immutable
 @NotNullByDefault
 class BlogProtocolEngineImpl extends ProtocolEngineImpl<Blog> {
@@ -31,13 +35,14 @@ class BlogProtocolEngineImpl extends ProtocolEngineImpl<Blog> {
 			invitationFactory;
 
 	@Inject
-	BlogProtocolEngineImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MessageEncoder messageEncoder,
-			MessageParser<Blog> messageParser, MessageTracker messageTracker,
-			Clock clock, BlogManager blogManager,
+	BlogProtocolEngineImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MessageEncoder messageEncoder, MessageParser<Blog> messageParser,
+			MessageTracker messageTracker, Clock clock, BlogManager blogManager,
 			InvitationFactory<Blog, BlogInvitationResponse> invitationFactory) {
-		super(db, clientHelper, messageEncoder, messageParser, messageTracker,
-				clock);
+		super(db, clientHelper, clientVersioningManager, messageEncoder,
+				messageParser, messageTracker, clock, CLIENT_ID,
+				MAJOR_VERSION);
 		this.blogManager = blogManager;
 		this.invitationFactory = invitationFactory;
 	}
@@ -46,8 +51,8 @@ class BlogProtocolEngineImpl extends ProtocolEngineImpl<Blog> {
 	Event getInvitationRequestReceivedEvent(InviteMessage<Blog> m,
 			ContactId contactId, boolean available, boolean canBeOpened) {
 		InvitationRequest<Blog> request = invitationFactory
-						.createInvitationRequest(false, false, true, false, m,
-								contactId, available, canBeOpened);
+				.createInvitationRequest(false, false, true, false, m,
+						contactId, available, canBeOpened);
 		return new BlogInvitationRequestReceivedEvent(m.getShareable(),
 				contactId, request);
 	}
@@ -74,7 +79,7 @@ class BlogProtocolEngineImpl extends ProtocolEngineImpl<Blog> {
 
 	@Override
 	protected ClientId getShareableClientId() {
-		return BlogManager.CLIENT_ID;
+		return CLIENT_ID;
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
index f0086e856c0a12c99fa85a2394592d6b3a2333cc..5c77c7be942df58dd618bfc5fd70d055699d4bc9 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
 import org.briarproject.briar.api.blog.BlogManager;
@@ -32,6 +33,7 @@ class BlogSharingManagerImpl extends SharingManagerImpl<Blog>
 
 	@Inject
 	BlogSharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser, MessageParser<Blog> messageParser,
 			SessionEncoder sessionEncoder, SessionParser sessionParser,
 			MessageTracker messageTracker,
@@ -39,9 +41,9 @@ class BlogSharingManagerImpl extends SharingManagerImpl<Blog>
 			ProtocolEngine<Blog> engine,
 			InvitationFactory<Blog, BlogInvitationResponse> invitationFactory,
 			IdentityManager identityManager, BlogManager blogManager) {
-		super(db, clientHelper, metadataParser, messageParser, sessionEncoder,
-				sessionParser, messageTracker, contactGroupFactory, engine,
-				invitationFactory);
+		super(db, clientHelper, clientVersioningManager, metadataParser,
+				messageParser, sessionEncoder, sessionParser, messageTracker,
+				contactGroupFactory, engine, invitationFactory);
 		this.identityManager = identityManager;
 		this.blogManager = blogManager;
 	}
@@ -52,27 +54,31 @@ class BlogSharingManagerImpl extends SharingManagerImpl<Blog>
 	}
 
 	@Override
-	protected int getClientVersion() {
-		return CLIENT_VERSION;
+	protected int getMajorVersion() {
+		return MAJOR_VERSION;
 	}
 
-	/**
-	 * This is called during each startup for each existing Contact.
-	 */
 	@Override
-	public void addingContact(Transaction txn, Contact c) throws DbException {
-		// Return if we've already set things up for this contact
-		if (db.containsGroup(txn, getContactGroup(c).getId())) return;
+	protected ClientId getShareableClientId() {
+		return BlogManager.CLIENT_ID;
+	}
 
-		// creates a group to share with the contact
+	@Override
+	protected int getShareableMajorVersion() {
+		return BlogManager.MAJOR_VERSION;
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
 		super.addingContact(txn, c);
 
-		// get our blog and that of Contact c
+		// Get our blog and that of the contact
 		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
 		Blog ourBlog = blogManager.getPersonalBlog(localAuthor);
 		Blog theirBlog = blogManager.getPersonalBlog(c.getAuthor());
 
-		// pre-share both blogs, if they have not been shared already
+		// Pre-share both blogs, if they have not been shared already
 		try {
 			preShareShareable(txn, c, ourBlog);
 			preShareShareable(txn, c, theirBlog);
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java
index 9fb695e50718ca5686841a8a133184bdd0a0d7a2..7b2a1c7854d2bb56e8802d65929dd7bc3fa164d0 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.forum.Forum;
 import org.briarproject.briar.api.forum.ForumInvitationResponse;
@@ -22,6 +23,9 @@ import org.briarproject.briar.api.sharing.InvitationRequest;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
+import static org.briarproject.briar.api.forum.ForumManager.MAJOR_VERSION;
+
 @Immutable
 @NotNullByDefault
 class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> {
@@ -32,12 +36,15 @@ class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> {
 
 	@Inject
 	ForumProtocolEngineImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MessageEncoder messageEncoder,
-			MessageParser<Forum> messageParser, MessageTracker messageTracker,
-			Clock clock, ForumManager forumManager,
+			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MessageEncoder messageEncoder, MessageParser<Forum> messageParser,
+			MessageTracker messageTracker, Clock clock,
+			ForumManager forumManager,
 			InvitationFactory<Forum, ForumInvitationResponse> invitationFactory) {
-		super(db, clientHelper, messageEncoder, messageParser, messageTracker,
-				clock);
+		super(db, clientHelper, clientVersioningManager, messageEncoder,
+				messageParser, messageTracker, clock, CLIENT_ID,
+				MAJOR_VERSION);
 		this.forumManager = forumManager;
 		this.invitationFactory = invitationFactory;
 	}
@@ -46,8 +53,8 @@ class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> {
 	Event getInvitationRequestReceivedEvent(InviteMessage<Forum> m,
 			ContactId contactId, boolean available, boolean canBeOpened) {
 		InvitationRequest<Forum> request = invitationFactory
-						.createInvitationRequest(false, false, true, false, m,
-								contactId, available, canBeOpened);
+				.createInvitationRequest(false, false, true, false, m,
+						contactId, available, canBeOpened);
 		return new ForumInvitationRequestReceivedEvent(m.getShareable(),
 				contactId, request);
 	}
@@ -74,7 +81,7 @@ class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> {
 
 	@Override
 	protected ClientId getShareableClientId() {
-		return ForumManager.CLIENT_ID;
+		return CLIENT_ID;
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
index 528743b9cc6ba65d939f8db3c27ab7014d8e3b36..9b3cd97e16dd9c3893dcf26be9381473f4a53b46 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
@@ -8,9 +8,11 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.forum.Forum;
 import org.briarproject.briar.api.forum.ForumInvitationResponse;
+import org.briarproject.briar.api.forum.ForumManager;
 import org.briarproject.briar.api.forum.ForumManager.RemoveForumHook;
 import org.briarproject.briar.api.forum.ForumSharingManager;
 
@@ -22,15 +24,16 @@ class ForumSharingManagerImpl extends SharingManagerImpl<Forum>
 
 	@Inject
 	ForumSharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser, MessageParser<Forum> messageParser,
 			SessionEncoder sessionEncoder, SessionParser sessionParser,
 			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
 			ProtocolEngine<Forum> engine,
 			InvitationFactory<Forum, ForumInvitationResponse> invitationFactory) {
-		super(db, clientHelper, metadataParser, messageParser, sessionEncoder,
-				sessionParser, messageTracker, contactGroupFactory, engine,
-				invitationFactory);
+		super(db, clientHelper, clientVersioningManager, metadataParser,
+				messageParser, sessionEncoder, sessionParser, messageTracker,
+				contactGroupFactory, engine, invitationFactory);
 	}
 
 	@Override
@@ -39,8 +42,18 @@ class ForumSharingManagerImpl extends SharingManagerImpl<Forum>
 	}
 
 	@Override
-	protected int getClientVersion() {
-		return CLIENT_VERSION;
+	protected int getMajorVersion() {
+		return MAJOR_VERSION;
+	}
+
+	@Override
+	protected ClientId getShareableClientId() {
+		return ForumManager.CLIENT_ID;
+	}
+
+	@Override
+	protected int getShareableMajorVersion() {
+		return ForumManager.MAJOR_VERSION;
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
index e6a7976d5829b21478ae15dbb11d476cb740b7bf..0a672f57810a031537abae10dfc11c7a7db9b20a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.sharing.Shareable;
@@ -52,19 +53,27 @@ abstract class ProtocolEngineImpl<S extends Shareable>
 	protected final ClientHelper clientHelper;
 	protected final MessageParser<S> messageParser;
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final MessageEncoder messageEncoder;
 	private final MessageTracker messageTracker;
 	private final Clock clock;
+	private final ClientId shareableClientId;
+	private final int shareableClientVersion;
 
 	ProtocolEngineImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MessageEncoder messageEncoder, MessageParser<S> messageParser,
-			MessageTracker messageTracker, Clock clock) {
+			MessageTracker messageTracker, Clock clock,
+			ClientId shareableClientId, int shareableClientVersion) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.messageEncoder = messageEncoder;
 		this.messageParser = messageParser;
 		this.messageTracker = messageTracker;
 		this.clock = clock;
+		this.shareableClientId = shareableClientId;
+		this.shareableClientVersion = shareableClientVersion;
 	}
 
 	@Override
@@ -598,9 +607,13 @@ abstract class ProtocolEngineImpl<S extends Shareable>
 	}
 
 	private void setShareableVisibility(Transaction txn, Session session,
-			Visibility v) throws DbException, FormatException {
+			Visibility preferred) throws DbException, FormatException {
+		// Apply min of preferred visibility and client's visibility
 		ContactId contactId = getContactId(txn, session.getContactGroupId());
-		db.setGroupVisibility(txn, contactId, session.getShareableId(), v);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				contactId, shareableClientId, shareableClientVersion);
+		Visibility min = Visibility.min(preferred, client);
+		db.setGroupVisibility(txn, contactId, session.getShareableId(), min);
 	}
 
 	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java
index 7cf2bf139d67495092dec01dc158674aec217a63..641731a4aa9180cbb7dcb62545866ddce137b014 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java
@@ -9,6 +9,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -27,6 +28,7 @@ class SessionEncoderImpl implements SessionEncoder {
 	@Override
 	public BdfDictionary encodeSession(Session s) {
 		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_IS_SESSION, true);
 		d.put(SESSION_KEY_SESSION_ID, s.getShareableId());
 		d.put(SESSION_KEY_SHAREABLE_ID, s.getShareableId());
 		MessageId lastLocalMessageId = s.getLastLocalMessageId();
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
index a82aed3eca199a2eb8eb2924375595a88ba42a0e..eec97355f4db745ea8fb54712cd73717698de051 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
@@ -11,6 +11,8 @@ interface SessionParser {
 
 	BdfDictionary getSessionQuery(SessionId s);
 
+	BdfDictionary getAllSessionsQuery();
+
 	Session parseSession(GroupId contactGroupId, BdfDictionary d)
 			throws FormatException;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
index 62d9c44b12b276ea27655471b916451597ceedcb..ee0c06f61f32d6acb9149d6664b927cde30eb14f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
@@ -13,6 +13,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -33,6 +34,11 @@ class SessionParserImpl implements SessionParser {
 		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
 	}
 
+	@Override
+	public BdfDictionary getAllSessionsQuery() {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_IS_SESSION, true));
+	}
+
 	@Override
 	public Session parseSession(GroupId contactGroupId,
 			BdfDictionary d) throws FormatException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
index d98099e27b2ff30a024ce7a2efbe4b72c01c26e1..f2522d1a9bb05a51a2d68d2a7f29db9153d832a4 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
@@ -18,6 +18,7 @@ interface SharingConstants {
 	String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted";
 
 	// Session keys
+	String SESSION_KEY_IS_SESSION = "isSession";
 	String SESSION_KEY_STATE = "state";
 	String SESSION_KEY_SESSION_ID = "sessionId";
 	String SESSION_KEY_SHAREABLE_ID = "shareableId";
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
index f26996438d7bce6a75e2e276f90f8020d4668c3c..fc57a6daace13c60480ea5e1c4e2518b4920ad1f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
@@ -17,10 +17,13 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.MessageStatus;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationMessage;
@@ -52,8 +55,10 @@ import static org.briarproject.briar.sharing.State.SHARING;
 @NotNullByDefault
 abstract class SharingManagerImpl<S extends Shareable>
 		extends ConversationClientImpl
-		implements SharingManager<S>, Client, ContactHook {
+		implements SharingManager<S>, Client, ContactHook,
+		ClientVersioningHook {
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final MessageParser<S> messageParser;
 	private final SessionEncoder sessionEncoder;
 	private final SessionParser sessionParser;
@@ -62,12 +67,14 @@ abstract class SharingManagerImpl<S extends Shareable>
 	private final InvitationFactory<S, ?> invitationFactory;
 
 	SharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			MetadataParser metadataParser, MessageParser<S> messageParser,
 			SessionEncoder sessionEncoder, SessionParser sessionParser,
 			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory, ProtocolEngine<S> engine,
 			InvitationFactory<S, ?> invitationFactory) {
 		super(db, clientHelper, metadataParser, messageTracker);
+		this.clientVersioningManager = clientVersioningManager;
 		this.messageParser = messageParser;
 		this.sessionEncoder = sessionEncoder;
 		this.sessionParser = sessionParser;
@@ -78,48 +85,51 @@ abstract class SharingManagerImpl<S extends Shareable>
 
 	protected abstract ClientId getClientId();
 
-	protected abstract int getClientVersion();
+	protected abstract int getMajorVersion();
+
+	protected abstract ClientId getShareableClientId();
+
+	protected abstract int getShareableMajorVersion();
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
 		// Create a local group to indicate that we've set this client up
 		Group localGroup = contactGroupFactory.createLocalGroup(getClientId(),
-				getClientVersion());
+				getMajorVersion());
 		if (db.containsGroup(txn, localGroup.getId())) return;
 		db.addGroup(txn, localGroup);
-		// Ensure we've set things up for any pre-existing contacts
+		// Set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		db.addGroup(txn, g);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), getClientId(), getMajorVersion());
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
+		// Attach the contact ID to the group
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
 		try {
-			// Create a group to share with the contact
-			Group g = getContactGroup(c);
-			// Return if we've already set things up for this contact
-			if (db.containsGroup(txn, g.getId())) return;
-			// Store the group and share it with the contact
-			db.addGroup(txn, g);
-			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
-			// Attach the contact ID to the group
-			BdfDictionary meta = new BdfDictionary();
-			meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
 			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
 		} catch (FormatException e) {
-			throw new DbException(e);
+			throw new AssertionError(e);
 		}
 	}
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
-		// remove the contact group (all messages will be removed with it)
+		// Remove the contact group (all messages will be removed with it)
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	@Override
 	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(getClientId(),
-				getClientVersion(), c);
+				getMajorVersion(), c);
 	}
 
 	@Override
@@ -152,17 +162,21 @@ abstract class SharingManagerImpl<S extends Shareable>
 	 */
 	void preShareShareable(Transaction txn, Contact c, S shareable)
 			throws DbException, FormatException {
-		// return if a session already exists with that Contact
+		// Return if a session already exists with the contact
 		GroupId contactGroupId = getContactGroup(c).getId();
 		StoredSession existingSession = getSession(txn, contactGroupId,
 				getSessionId(shareable.getId()));
 		if (existingSession != null) return;
 
-		// add and shares the shareable with the Contact
+		// Add the shareable
 		db.addGroup(txn, shareable.getGroup());
-		db.setGroupVisibility(txn, c.getId(), shareable.getId(), SHARED);
 
-		// initialize session in sharing state
+		// Apply the client's visibility
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), getShareableClientId(), getShareableMajorVersion());
+		db.setGroupVisibility(txn, c.getId(), shareable.getId(), client);
+
+		// Initialize session in sharing state
 		Session session = new Session(SHARING, contactGroupId,
 				shareable.getId(), null, null, 0, 0);
 		MessageId storageId = createStorageId(txn, contactGroupId);
@@ -446,6 +460,10 @@ abstract class SharingManagerImpl<S extends Shareable>
 
 	private boolean canBeShared(Transaction txn, GroupId g, Contact c)
 			throws DbException {
+		// The group can't be shared unless the contact supports the client
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), getShareableClientId(), getShareableMajorVersion());
+		if (client != SHARED) return false;
 		GroupId contactGroupId = getContactGroup(c).getId();
 		SessionId sessionId = getSessionId(g);
 		try {
@@ -482,6 +500,51 @@ abstract class SharingManagerImpl<S extends Shareable>
 		}
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
+	ClientVersioningHook getShareableClientVersioningHook() {
+		return this::onShareableClientVisibilityChanging;
+	}
+
+	// Versioning hook for the shareable client
+	private void onShareableClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility client) throws DbException {
+		try {
+			Collection<Group> shareables = db.getGroups(txn,
+					getShareableClientId(), getShareableMajorVersion());
+			Map<GroupId, Visibility> m = getPreferredVisibilities(txn, c);
+			for (Group g : shareables) {
+				Visibility preferred = m.get(g.getId());
+				if (preferred == null) continue; // No session for this group
+				// Apply min of preferred visibility and client's visibility
+				Visibility min = Visibility.min(preferred, client);
+				db.setGroupVisibility(txn, c.getId(), g.getId(), min);
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private Map<GroupId, Visibility> getPreferredVisibilities(Transaction txn,
+			Contact c) throws DbException, FormatException {
+		GroupId contactGroupId = getContactGroup(c).getId();
+		BdfDictionary query = sessionParser.getAllSessionsQuery();
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+		Map<GroupId, Visibility> m = new HashMap<>();
+		for (BdfDictionary d : results.values()) {
+			Session s = sessionParser.parseSession(contactGroupId, d);
+			m.put(s.getShareableId(), s.getState().getVisibility());
+		}
+		return m;
+	}
+
 	private static class StoredSession {
 
 		private final MessageId storageId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
index 64d4d0d37d6fdeb10feb672c9bbe59b6ae2ad4dd..c5c1baccef09ed2fafe1ae89e96431d2ce1c1d6d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogFactory;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
@@ -59,11 +60,11 @@ public class SharingModule {
 			ValidationManager validationManager, MessageEncoder messageEncoder,
 			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
 			Clock clock, BlogFactory blogFactory) {
-		BlogSharingValidator validator =
-				new BlogSharingValidator(messageEncoder, clientHelper,
-						metadataEncoder, clock, blogFactory);
+		BlogSharingValidator validator = new BlogSharingValidator(
+				messageEncoder, clientHelper, metadataEncoder, clock,
+				blogFactory);
 		validationManager.registerMessageValidator(BlogSharingManager.CLIENT_ID,
-				validator);
+				BlogSharingManager.MAJOR_VERSION, validator);
 		return validator;
 	}
 
@@ -73,14 +74,23 @@ public class SharingModule {
 			LifecycleManager lifecycleManager, ContactManager contactManager,
 			ValidationManager validationManager,
 			ConversationManager conversationManager, BlogManager blogManager,
+			ClientVersioningManager clientVersioningManager,
 			BlogSharingManagerImpl blogSharingManager) {
 		lifecycleManager.registerClient(blogSharingManager);
 		contactManager.registerContactHook(blogSharingManager);
 		validationManager.registerIncomingMessageHook(
-				BlogSharingManager.CLIENT_ID, blogSharingManager);
+				BlogSharingManager.CLIENT_ID, BlogSharingManager.MAJOR_VERSION,
+				blogSharingManager);
 		conversationManager.registerConversationClient(blogSharingManager);
 		blogManager.registerRemoveBlogHook(blogSharingManager);
-
+		clientVersioningManager.registerClient(BlogSharingManager.CLIENT_ID,
+				BlogSharingManager.MAJOR_VERSION,
+				BlogSharingManager.MINOR_VERSION, blogSharingManager);
+		// The blog sharing manager handles client visibility changes for the
+		// blog manager
+		clientVersioningManager.registerClient(BlogManager.CLIENT_ID,
+				BlogManager.MAJOR_VERSION, BlogManager.MINOR_VERSION,
+				blogSharingManager.getShareableClientVersioningHook());
 		return blogSharingManager;
 	}
 
@@ -108,12 +118,12 @@ public class SharingModule {
 			ValidationManager validationManager, MessageEncoder messageEncoder,
 			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
 			Clock clock, ForumFactory forumFactory) {
-		ForumSharingValidator validator =
-				new ForumSharingValidator(messageEncoder, clientHelper,
-						metadataEncoder, clock, forumFactory);
-		validationManager
-				.registerMessageValidator(ForumSharingManager.CLIENT_ID,
-						validator);
+		ForumSharingValidator validator = new ForumSharingValidator(
+				messageEncoder, clientHelper, metadataEncoder, clock,
+				forumFactory);
+		validationManager.registerMessageValidator(
+				ForumSharingManager.CLIENT_ID,
+				ForumSharingManager.MAJOR_VERSION, validator);
 		return validator;
 	}
 
@@ -123,15 +133,23 @@ public class SharingModule {
 			LifecycleManager lifecycleManager, ContactManager contactManager,
 			ValidationManager validationManager,
 			ConversationManager conversationManager, ForumManager forumManager,
+			ClientVersioningManager clientVersioningManager,
 			ForumSharingManagerImpl forumSharingManager) {
-
 		lifecycleManager.registerClient(forumSharingManager);
 		contactManager.registerContactHook(forumSharingManager);
 		validationManager.registerIncomingMessageHook(
-				ForumSharingManager.CLIENT_ID, forumSharingManager);
+				ForumSharingManager.CLIENT_ID,
+				ForumSharingManager.MAJOR_VERSION, forumSharingManager);
 		conversationManager.registerConversationClient(forumSharingManager);
 		forumManager.registerRemoveForumHook(forumSharingManager);
-
+		clientVersioningManager.registerClient(ForumSharingManager.CLIENT_ID,
+				ForumSharingManager.MAJOR_VERSION,
+				ForumSharingManager.MINOR_VERSION, forumSharingManager);
+		// The forum sharing manager handles client visibility changes for the
+		// forum manager
+		clientVersioningManager.registerClient(ForumManager.CLIENT_ID,
+				ForumManager.MAJOR_VERSION, ForumManager.MINOR_VERSION,
+				forumSharingManager.getShareableClientVersioningHook());
 		return forumSharingManager;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/State.java b/briar-core/src/main/java/org/briarproject/briar/sharing/State.java
index ade7d7a248787123e6a10bd8cfcc43cc4ba51210..2d0ed97660d9af2cbb2c79b4e47cde5e2537b01c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/State.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/State.java
@@ -2,26 +2,41 @@ package org.briarproject.briar.sharing;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+
 @Immutable
 @NotNullByDefault
 enum State {
 
-	START(0), LOCAL_INVITED(1), REMOTE_INVITED(2), SHARING(3), LOCAL_LEFT(4),
-	REMOTE_HANGING(5);
+	START(0, INVISIBLE),
+	LOCAL_INVITED(1, INVISIBLE),
+	REMOTE_INVITED(2, VISIBLE),
+	SHARING(3, SHARED),
+	LOCAL_LEFT(4, INVISIBLE),
+	REMOTE_HANGING(5, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	State(int value) {
+	State(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	public int getValue() {
 		return value;
 	}
 
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	public boolean canInvite() {
 		return this == START;
 	}
diff --git a/briar-core/src/test/java/org/briarproject/briar/blog/BlogManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/blog/BlogManagerImplTest.java
index bbd905705a69572321662a4be9e79edbc69b9c80..524b7ea39b3909758252bd5595c396b78ba3fa05 100644
--- a/briar-core/src/test/java/org/briarproject/briar/blog/BlogManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/blog/BlogManagerImplTest.java
@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.blog.Blog;
@@ -49,6 +50,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
 import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_COMMENT_LENGTH;
 import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
 import static org.briarproject.briar.api.blog.MessageType.COMMENT;
 import static org.briarproject.briar.api.blog.MessageType.POST;
 import static org.briarproject.briar.api.blog.MessageType.WRAPPED_COMMENT;
@@ -866,7 +868,8 @@ public class BlogManagerImplTest extends BriarTestCase {
 	}
 
 	private Blog createBlog(LocalAuthor localAuthor, boolean rssFeed) {
-		return new Blog(getGroup(CLIENT_ID), localAuthor, rssFeed);
+		Group group = getGroup(CLIENT_ID, MAJOR_VERSION);
+		return new Blog(group, localAuthor, rssFeed);
 	}
 
 	private BdfList authorToBdfList(Author a) {
diff --git a/briar-core/src/test/java/org/briarproject/briar/blog/BlogPostValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/blog/BlogPostValidatorTest.java
index f2ca321d7f5c872ed0824db6e06dc1a7e4a2f6ca..7d00fb4138d7c4ed580f4459e79cdd4c9f3838ff 100644
--- a/briar-core/src/test/java/org/briarproject/briar/blog/BlogPostValidatorTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/blog/BlogPostValidatorTest.java
@@ -36,7 +36,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_PARENT_MSG_ID;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_READ;
 import static org.briarproject.briar.api.blog.BlogConstants.KEY_RSS_FEED;
 import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
-import static org.briarproject.briar.api.blog.BlogManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
 import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_COMMENT;
 import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_POST;
 import static org.briarproject.briar.api.blog.MessageType.COMMENT;
@@ -64,7 +64,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 	private final String body = getRandomString(42);
 
 	public BlogPostValidatorTest() {
-		group = getGroup(CLIENT_ID);
+		group = getGroup(CLIENT_ID, MAJOR_VERSION);
 		descriptor = group.getDescriptor();
 		author = getAuthor();
 		authorList = BdfList.of(
@@ -206,7 +206,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		byte[] originalBody = getRandomBytes(42);
 
 		context.checking(new Expectations() {{
-			oneOf(groupFactory).createGroup(CLIENT_ID, CLIENT_VERSION,
+			oneOf(groupFactory).createGroup(CLIENT_ID, MAJOR_VERSION,
 					descriptor);
 			will(returnValue(b.getGroup()));
 			oneOf(blogFactory).parseBlog(b.getGroup());
@@ -250,7 +250,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		byte[] originalBody = getRandomBytes(42);
 
 		context.checking(new Expectations() {{
-			oneOf(groupFactory).createGroup(CLIENT_ID, CLIENT_VERSION,
+			oneOf(groupFactory).createGroup(CLIENT_ID, MAJOR_VERSION,
 					descriptor);
 			will(returnValue(blog.getGroup()));
 			oneOf(clientHelper).toByteArray(originalList);
diff --git a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerImplTest.java
index 05a17fee5f776a51997a6a92cd93036b5d936108..4f104feb9b46016c44e344d76267669755657174 100644
--- a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerImplTest.java
@@ -43,7 +43,7 @@ import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEEDS;
 import static org.briarproject.briar.api.feed.FeedManager.CLIENT_ID;
-import static org.briarproject.briar.api.feed.FeedManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.feed.FeedManager.MAJOR_VERSION;
 
 public class FeedManagerImplTest extends BrambleMockTestCase {
 
@@ -61,9 +61,10 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
 	private final Clock clock = context.mock(Clock.class);
 	private final Dns noDnsLookups = context.mock(Dns.class);
 
-	private final Group localGroup = getGroup(CLIENT_ID);
+	private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 	private final GroupId localGroupId = localGroup.getId();
-	private final Group blogGroup = getGroup(BlogManager.CLIENT_ID);
+	private final Group blogGroup =
+			getGroup(BlogManager.CLIENT_ID, BlogManager.MAJOR_VERSION);
 	private final GroupId blogGroupId = blogGroup.getId();
 	private final LocalAuthor localAuthor = getLocalAuthor();
 	private final Blog blog = new Blog(blogGroup, localAuthor, true);
@@ -131,7 +132,7 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
 	private void expectGetLocalGroup() {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 		}});
 	}
diff --git a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTest.java
index 2fc24aae58f3f082491e5ca6011ca7ca05d9100a..24c7334e72210cc96fa46ac4dfb4a379d46542ba 100644
--- a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTest.java
@@ -2,7 +2,7 @@ package org.briarproject.briar.feed;
 
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.contact.ContactModule;
-import org.briarproject.bramble.crypto.CryptoModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
 import org.briarproject.bramble.sync.SyncModule;
@@ -10,6 +10,7 @@ import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.blog.BlogPostHeader;
@@ -113,15 +114,16 @@ public class FeedManagerIntegrationTest extends BriarTestCase {
 
 	protected void injectEagerSingletons(
 			FeedManagerIntegrationTestComponent component) {
-		component.inject(new FeedModule.EagerSingletons());
 		component.inject(new BlogModule.EagerSingletons());
 		component.inject(new ContactModule.EagerSingletons());
-		component.inject(new CryptoModule.EagerSingletons());
+		component.inject(new CryptoExecutorModule.EagerSingletons());
+		component.inject(new FeedModule.EagerSingletons());
 		component.inject(new IdentityModule.EagerSingletons());
 		component.inject(new LifecycleModule.EagerSingletons());
 		component.inject(new SyncModule.EagerSingletons());
 		component.inject(new SystemModule.EagerSingletons());
 		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new VersioningModule.EagerSingletons());
 	}
 
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
index 0a8fbfbe62db6c661dcfbe89008aa432ede1b055..76445fe03d18bc67993a40f2aa2a2a9d3ccf6225 100644
--- a/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/feed/FeedManagerIntegrationTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.feed;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.client.ClientModule;
 import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.crypto.CryptoModule;
 import org.briarproject.bramble.data.DataModule;
 import org.briarproject.bramble.db.DatabaseModule;
@@ -16,6 +17,7 @@ import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
 import org.briarproject.bramble.test.TestSocksModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.feed.FeedManager;
 import org.briarproject.briar.blog.BlogModule;
@@ -33,32 +35,34 @@ import dagger.Component;
 		TestSecureRandomModule.class,
 		TestSocksModule.class,
 		TestDnsModule.class,
-		LifecycleModule.class,
 		BriarClientModule.class,
 		ClientModule.class,
 		ContactModule.class,
 		CryptoModule.class,
+		CryptoExecutorModule.class,
 		BlogModule.class,
 		FeedModule.class,
 		DataModule.class,
 		DatabaseModule.class,
 		EventModule.class,
 		IdentityModule.class,
+		LifecycleModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		VersioningModule.class
 })
 interface FeedManagerIntegrationTestComponent {
 
 	void inject(FeedManagerIntegrationTest testCase);
 
-	void inject(FeedModule.EagerSingletons init);
-
 	void inject(BlogModule.EagerSingletons init);
 
 	void inject(ContactModule.EagerSingletons init);
 
-	void inject(CryptoModule.EagerSingletons init);
+	void inject(CryptoExecutorModule.EagerSingletons init);
+
+	void inject(FeedModule.EagerSingletons init);
 
 	void inject(IdentityModule.EagerSingletons init);
 
@@ -70,6 +74,8 @@ interface FeedManagerIntegrationTestComponent {
 
 	void inject(TransportModule.EagerSingletons init);
 
+	void inject(VersioningModule.EagerSingletons init);
+
 	LifecycleManager getLifecycleManager();
 
 	FeedManager getFeedManager();
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index 7441186e8ba3bc017e294bf826ee11bf7596085a..f860e702e0ad032579ef1e528b2377879d8c083d 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -47,7 +47,7 @@ import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
 import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
 import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES;
 import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED;
 import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
@@ -1284,7 +1284,7 @@ public class IntroductionIntegrationTest
 	}
 
 	private Group getLocalGroup() {
-		return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
+		return contactGroupFactory.createLocalGroup(CLIENT_ID, MAJOR_VERSION);
 	}
 
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index 3a90d7d148af021f0744cf0a3f67502c324e2f78..160d80e534c9d3b234835ad0f08b8293c4724056 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -2,6 +2,7 @@ package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.client.ClientModule;
 import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.crypto.CryptoModule;
 import org.briarproject.bramble.data.DataModule;
 import org.briarproject.bramble.db.DatabaseModule;
@@ -15,6 +16,7 @@ import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
@@ -38,6 +40,7 @@ import dagger.Component;
 		ClientModule.class,
 		ContactModule.class,
 		CryptoModule.class,
+		CryptoExecutorModule.class,
 		DataModule.class,
 		DatabaseModule.class,
 		EventModule.class,
@@ -52,7 +55,8 @@ import dagger.Component;
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		VersioningModule.class
 })
 interface IntroductionIntegrationTestComponent
 		extends BriarIntegrationTestComponent {
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
index 7b15b6ab43e665f52c99af222f833298a758c161..a29f7944577f3b494ba3eb50a6ea44dcf26290c1 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
@@ -25,12 +25,14 @@ import javax.inject.Inject;
 import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
 import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
 import static org.briarproject.briar.introduction.MessageType.ABORT;
 import static org.briarproject.briar.introduction.MessageType.REQUEST;
 import static org.briarproject.briar.test.BriarTestUtils.getRealAuthor;
@@ -57,8 +59,8 @@ public class MessageEncoderParserIntegrationTest extends BrambleTestCase {
 	private final MessageParser messageParser;
 	private final IntroductionValidator validator;
 
-	private final GroupId groupId = new GroupId(getRandomId());
-	private final Group group = new Group(groupId, CLIENT_ID, getRandomId());
+	private final Group group = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final GroupId groupId = group.getId();
 	private final long timestamp = 42L;
 	private final SessionId sessionId = new SessionId(getRandomId());
 	private final MessageId previousMsgId = new MessageId(getRandomId());
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
index b56d42d7fa993e4929361bf760faa85845eae326..c5a1f1851cb99e3acf668e9d400cd6d031e465c1 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
@@ -7,11 +7,18 @@ import org.briarproject.bramble.api.identity.AuthorFactory;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
+import org.briarproject.bramble.identity.IdentityModule;
+import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
+import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.forum.ForumPost;
 import org.briarproject.briar.api.forum.ForumPostFactory;
 import org.briarproject.briar.api.messaging.PrivateMessage;
 import org.briarproject.briar.api.messaging.PrivateMessageFactory;
+import org.briarproject.briar.forum.ForumModule;
 import org.briarproject.briar.test.BriarTestCase;
 import org.junit.Test;
 
@@ -85,6 +92,14 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
 
 	private static void injectEagerSingletons(
 			MessageSizeIntegrationTestComponent component) {
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new CryptoExecutorModule.EagerSingletons());
+		component.inject(new ForumModule.EagerSingletons());
+		component.inject(new IdentityModule.EagerSingletons());
+		component.inject(new MessagingModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
 		component.inject(new SystemModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new VersioningModule.EagerSingletons());
 	}
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
index 42b27da6f3c4265d021bb376316f65cc2f1944ab..01caa2e4b442bd8a1aeec9ac2eb5dabd87ed6b8b 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTestComponent.java
@@ -1,6 +1,8 @@
 package org.briarproject.briar.messaging;
 
 import org.briarproject.bramble.client.ClientModule;
+import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.crypto.CryptoModule;
 import org.briarproject.bramble.data.DataModule;
 import org.briarproject.bramble.db.DatabaseModule;
@@ -10,7 +12,10 @@ import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestLifecycleModule;
+import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
+import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
 
@@ -22,10 +27,13 @@ import dagger.Component;
 @Component(modules = {
 		TestDatabaseModule.class,
 		TestLifecycleModule.class,
+		TestPluginConfigModule.class,
 		TestSecureRandomModule.class,
 		BriarClientModule.class,
 		ClientModule.class,
+		ContactModule.class,
 		CryptoModule.class,
+		CryptoExecutorModule.class,
 		DataModule.class,
 		DatabaseModule.class,
 		EventModule.class,
@@ -33,11 +41,29 @@ import dagger.Component;
 		IdentityModule.class,
 		MessagingModule.class,
 		SyncModule.class,
-		SystemModule.class
+		SystemModule.class,
+		TransportModule.class,
+		VersioningModule.class
 })
 interface MessageSizeIntegrationTestComponent {
 
 	void inject(MessageSizeIntegrationTest testCase);
 
+	void inject(ContactModule.EagerSingletons init);
+
+	void inject(CryptoExecutorModule.EagerSingletons init);
+
+	void inject(ForumModule.EagerSingletons init);
+
+	void inject(IdentityModule.EagerSingletons init);
+
+	void inject(MessagingModule.EagerSingletons init);
+
+	void inject(SyncModule.EagerSingletons init);
+
 	void inject(SystemModule.EagerSingletons init);
+
+	void inject(TransportModule.EagerSingletons init);
+
+	void inject(VersioningModule.EagerSingletons init);
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
index 8967961ceef7703a7bb38d07b0d346a0b1e61560..879b763daec59a93251164b0cfbdaa151b3cde66 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -12,17 +13,23 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.SyncSession;
 import org.briarproject.bramble.api.sync.SyncSessionFactory;
-import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
 import org.briarproject.bramble.api.transport.KeyManager;
 import org.briarproject.bramble.api.transport.StreamContext;
 import org.briarproject.bramble.api.transport.StreamReaderFactory;
 import org.briarproject.bramble.api.transport.StreamWriterFactory;
+import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.identity.IdentityModule;
+import org.briarproject.bramble.lifecycle.LifecycleModule;
+import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.briarproject.briar.api.messaging.PrivateMessage;
 import org.briarproject.briar.api.messaging.PrivateMessageFactory;
+import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
 import org.briarproject.briar.test.BriarTestCase;
 import org.junit.After;
 import org.junit.Before;
@@ -41,7 +48,6 @@ import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
 import static org.briarproject.bramble.test.TestUtils.getSecretKey;
 import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -71,103 +77,105 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 
 	@Test
 	public void testWriteAndRead() throws Exception {
-		read(write());
+		// Set up the devices and get the contact IDs
+		ContactId bobId = setUp(alice, aliceAuthor, bobAuthor, true);
+		ContactId aliceId = setUp(bob, bobAuthor, aliceAuthor, false);
+		// Add a private message listener
+		PrivateMessageListener listener = new PrivateMessageListener();
+		bob.getEventBus().addListener(listener);
+		// Alice sends a private message to Bob
+		sendMessage(alice, bobId);
+		// Send three simplex streams to exchange client versions and the
+		// private message
+		read(bob, aliceId, write(alice, bobId));
+		read(alice, bobId, write(bob, aliceId));
+		read(bob, aliceId, write(alice, bobId));
+		// Tear down the devices
+		tearDown(alice);
+		tearDown(bob);
+		// Bob should have received the private message
+		assertTrue(listener.messageAdded);
 	}
 
-
-	private byte[] write() throws Exception {
-		// Instantiate Alice's services
-		LifecycleManager lifecycleManager = alice.getLifecycleManager();
-		IdentityManager identityManager = alice.getIdentityManager();
-		ContactManager contactManager = alice.getContactManager();
-		MessagingManager messagingManager = alice.getMessagingManager();
-		KeyManager keyManager = alice.getKeyManager();
-		PrivateMessageFactory privateMessageFactory =
-				alice.getPrivateMessageFactory();
-		StreamWriterFactory streamWriterFactory =
-				alice.getStreamWriterFactory();
-		SyncSessionFactory syncSessionFactory = alice.getSyncSessionFactory();
-
+	private ContactId setUp(SimplexMessagingIntegrationTestComponent device,
+			LocalAuthor local, Author remote, boolean alice) throws Exception {
 		// Start the lifecycle manager
+		LifecycleManager lifecycleManager = device.getLifecycleManager();
 		lifecycleManager.startServices(null);
 		lifecycleManager.waitForStartup();
-		// Add an identity for Alice
-		identityManager.registerLocalAuthor(aliceAuthor);
-		// Add Bob as a contact
-		ContactId contactId = contactManager.addContact(bobAuthor,
-				aliceAuthor.getId(), master, timestamp, true, true, true);
+		// Add an identity for the user
+		IdentityManager identityManager = device.getIdentityManager();
+		identityManager.registerLocalAuthor(local);
+		// Add the other user as a contact
+		ContactManager contactManager = device.getContactManager();
+		return contactManager.addContact(remote, local.getId(), master,
+				timestamp, alice, true, true);
+	}
 
+	private void sendMessage(SimplexMessagingIntegrationTestComponent device,
+			ContactId contactId) throws Exception {
 		// Send Bob a message
+		MessagingManager messagingManager = device.getMessagingManager();
 		GroupId groupId = messagingManager.getConversationId(contactId);
-		String body = "Hi Bob!";
+		PrivateMessageFactory privateMessageFactory =
+				device.getPrivateMessageFactory();
 		PrivateMessage message = privateMessageFactory.createPrivateMessage(
-				groupId, timestamp, body);
+				groupId, System.currentTimeMillis(), "Hi!");
 		messagingManager.addLocalMessage(message);
-		// Get a stream context
-		StreamContext ctx = keyManager.getStreamContext(contactId,
-				TRANSPORT_ID);
-		assertNotNull(ctx);
-		// Create a stream writer
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		OutputStream streamWriter = streamWriterFactory.createStreamWriter(
-				out, ctx);
-		// Create an outgoing sync session
-		SyncSession session = syncSessionFactory.createSimplexOutgoingSession(
-				contactId, MAX_LATENCY, streamWriter);
-		// Write whatever needs to be written
-		session.run();
-		streamWriter.close();
-
-		// Clean up
-		lifecycleManager.stopServices();
-		lifecycleManager.waitForShutdown();
-
-		// Return the contents of the stream
-		return out.toByteArray();
 	}
 
-	private void read(byte[] stream) throws Exception {
-		// Instantiate Bob's services
-		LifecycleManager lifecycleManager = bob.getLifecycleManager();
-		IdentityManager identityManager = bob.getIdentityManager();
-		ContactManager contactManager = bob.getContactManager();
-		KeyManager keyManager = bob.getKeyManager();
-		StreamReaderFactory streamReaderFactory = bob.getStreamReaderFactory();
-		SyncSessionFactory syncSessionFactory = bob.getSyncSessionFactory();
-
-		// Start the lifecyle manager
-		lifecycleManager.startServices(null);
-		lifecycleManager.waitForStartup();
-		// Add an identity for Bob
-		identityManager.registerLocalAuthor(bobAuthor);
-		// Add Alice as a contact
-		ContactId contactId = contactManager.addContact(aliceAuthor,
-				bobAuthor.getId(), master, timestamp, false, true, true);
-		// Set up an event listener
-		MessageListener listener = new MessageListener();
-		bob.getEventBus().addListener(listener);
+	private void read(SimplexMessagingIntegrationTestComponent device,
+			ContactId contactId, byte[] stream) throws Exception {
 		// Read and recognise the tag
 		ByteArrayInputStream in = new ByteArrayInputStream(stream);
 		byte[] tag = new byte[TAG_LENGTH];
 		int read = in.read(tag);
 		assertEquals(tag.length, read);
+		KeyManager keyManager = device.getKeyManager();
 		StreamContext ctx = keyManager.getStreamContext(TRANSPORT_ID, tag);
 		assertNotNull(ctx);
 		// Create a stream reader
+		StreamReaderFactory streamReaderFactory =
+				device.getStreamReaderFactory();
 		InputStream streamReader = streamReaderFactory.createStreamReader(
 				in, ctx);
 		// Create an incoming sync session
+		SyncSessionFactory syncSessionFactory = device.getSyncSessionFactory();
 		SyncSession session = syncSessionFactory.createIncomingSession(
 				contactId, streamReader);
-		// No messages should have been added yet
-		assertFalse(listener.messageAdded);
 		// Read whatever needs to be read
 		session.run();
 		streamReader.close();
-		// The private message from Alice should have been added
-		assertTrue(listener.messageAdded);
+	}
+
+	private byte[] write(SimplexMessagingIntegrationTestComponent device,
+			ContactId contactId) throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		// Get a stream context
+		KeyManager keyManager = device.getKeyManager();
+		StreamContext ctx = keyManager.getStreamContext(contactId,
+				TRANSPORT_ID);
+		assertNotNull(ctx);
+		// Create a stream writer
+		StreamWriterFactory streamWriterFactory =
+				device.getStreamWriterFactory();
+		OutputStream streamWriter =
+				streamWriterFactory.createStreamWriter(out, ctx);
+		// Create an outgoing sync session
+		SyncSessionFactory syncSessionFactory = device.getSyncSessionFactory();
+		SyncSession session = syncSessionFactory.createSimplexOutgoingSession(
+				contactId, MAX_LATENCY, streamWriter);
+		// Write whatever needs to be written
+		session.run();
+		streamWriter.close();
+		// Return the contents of the stream
+		return out.toByteArray();
+	}
 
-		// Clean up
+	private void tearDown(SimplexMessagingIntegrationTestComponent device)
+			throws Exception {
+		// Stop the lifecycle manager
+		LifecycleManager lifecycleManager = device.getLifecycleManager();
 		lifecycleManager.stopServices();
 		lifecycleManager.waitForShutdown();
 	}
@@ -179,18 +187,24 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 
 	private static void injectEagerSingletons(
 			SimplexMessagingIntegrationTestComponent component) {
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new IdentityModule.EagerSingletons());
+		component.inject(new LifecycleModule.EagerSingletons());
 		component.inject(new MessagingModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
 		component.inject(new SystemModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new VersioningModule.EagerSingletons());
 	}
 
 	@NotNullByDefault
-	private static class MessageListener implements EventListener {
+	private static class PrivateMessageListener implements EventListener {
 
 		private volatile boolean messageAdded = false;
 
 		@Override
 		public void eventOccurred(Event e) {
-			if (e instanceof MessageAddedEvent) messageAdded = true;
+			if (e instanceof PrivateMessageReceivedEvent) messageAdded = true;
 		}
 	}
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
index d740859aa130141e1c1fe5fee92fa3ad2e3ee8e0..f3d6f039977361459b26333f5ade3d6538db3657 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
@@ -18,10 +18,12 @@ import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
+import org.briarproject.bramble.test.TestCryptoExecutorModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.briarproject.briar.api.messaging.PrivateMessageFactory;
 import org.briarproject.briar.client.BriarClientModule;
@@ -32,6 +34,7 @@ import dagger.Component;
 
 @Singleton
 @Component(modules = {
+		TestCryptoExecutorModule.class,
 		TestDatabaseModule.class,
 		TestPluginConfigModule.class,
 		TestSecureRandomModule.class,
@@ -47,14 +50,27 @@ import dagger.Component;
 		MessagingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		VersioningModule.class
 })
 interface SimplexMessagingIntegrationTestComponent {
 
+	void inject(ContactModule.EagerSingletons init);
+
+	void inject(IdentityModule.EagerSingletons init);
+
+	void inject(LifecycleModule.EagerSingletons init);
+
 	void inject(MessagingModule.EagerSingletons init);
 
+	void inject(SyncModule.EagerSingletons init);
+
 	void inject(SystemModule.EagerSingletons init);
 
+	void inject(TransportModule.EagerSingletons init);
+
+	void inject(VersioningModule.EagerSingletons init);
+
 	LifecycleManager getLifecycleManager();
 
 	IdentityManager getIdentityManager();
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
index f7c630f655d37a903406ae069915c127e71f3c92..ebcaf70bb2dcf9112780f78041d2c3d4a6fc724b 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
@@ -11,10 +11,12 @@ import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -24,6 +26,7 @@ import org.briarproject.briar.api.privategroup.PrivateGroupManager;
 import org.jmock.Expectations;
 
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -33,6 +36,7 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.GROU
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
 import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.MAJOR_VERSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
 import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
@@ -46,6 +50,8 @@ public abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
 			context.mock(DatabaseComponent.class);
 	protected final ClientHelper clientHelper =
 			context.mock(ClientHelper.class);
+	protected final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	protected final PrivateGroupFactory privateGroupFactory =
 			context.mock(PrivateGroupFactory.class);
 	protected final PrivateGroupManager privateGroupManager =
@@ -64,7 +70,8 @@ public abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
 
 	protected final Transaction txn = new Transaction(null, false);
 	protected final GroupId contactGroupId = new GroupId(getRandomId());
-	protected final Group privateGroupGroup = getGroup(CLIENT_ID);
+	protected final Group privateGroupGroup =
+			getGroup(CLIENT_ID, MAJOR_VERSION);
 	protected final GroupId privateGroupId = privateGroupGroup.getId();
 	protected final Author author = getAuthor();
 	protected final PrivateGroup privateGroup =
@@ -179,10 +186,13 @@ public abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
 		}});
 	}
 
-	protected void expectSetPrivateGroupVisibility(Group.Visibility v)
+	protected void expectSetPrivateGroupVisibility(Visibility v)
 			throws Exception {
 		expectGetContactId();
 		context.checking(new Expectations() {{
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					CLIENT_ID, MAJOR_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, contactId, privateGroupId, v);
 		}});
 	}
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
index 22e64a9f98378e79d0ee242d9f24dbcca51474e3..c1b2bd9b2718b124e10455022425241c4a805dd8 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
@@ -19,9 +19,10 @@ import static org.junit.Assert.assertEquals;
 public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final CreatorProtocolEngine engine =
-			new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new CreatorProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 
 	private CreatorSession getDefaultSession(CreatorState state) {
 		return new CreatorSession(contactGroupId, privateGroupId,
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
index c225d056dc7f3436c0ec6e2c5fbdbc11437e1b96..d36fc02e399edfbf63980f186d518bf1c6a15608 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
@@ -19,6 +19,7 @@ import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.briar.api.client.MessageTracker;
@@ -55,7 +56,7 @@ import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
 import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
 import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
-import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
 import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
@@ -69,6 +70,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 
 	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
 	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	private final ContactGroupFactory contactGroupFactory =
 			context.mock(ContactGroupFactory.class);
 	private final PrivateGroupFactory privateGroupFactory =
@@ -99,9 +102,9 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 	private final Author author = getAuthor();
 	private final Contact contact = new Contact(contactId, author,
 			new AuthorId(getRandomId()), true, true);
-	private final Group localGroup = getGroup(CLIENT_ID);
-	private final Group contactGroup = getGroup(CLIENT_ID);
-	private final Group privateGroup = getGroup(CLIENT_ID);
+	private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Group privateGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 	private final BdfDictionary meta = BdfDictionary.of(new BdfEntry("m", "e"));
 	private final Message message =
 			new Message(new MessageId(getRandomId()), contactGroup.getId(),
@@ -140,18 +143,18 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 		}});
 		MetadataParser metadataParser = context.mock(MetadataParser.class);
 		MessageTracker messageTracker = context.mock(MessageTracker.class);
-		groupInvitationManager =
-				new GroupInvitationManagerImpl(db, clientHelper, metadataParser,
-						messageTracker, contactGroupFactory,
-						privateGroupFactory, privateGroupManager, messageParser,
-						sessionParser, sessionEncoder, engineFactory);
+		groupInvitationManager = new GroupInvitationManagerImpl(db,
+				clientHelper, clientVersioningManager, metadataParser,
+				messageTracker, contactGroupFactory, privateGroupFactory,
+				privateGroupManager, messageParser, sessionParser,
+				sessionEncoder, engineFactory);
 	}
 
 	@Test
 	public void testCreateLocalStateFirstTime() throws Exception {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 			oneOf(db).containsGroup(txn, localGroup.getId());
 			will(returnValue(false));
@@ -159,7 +162,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContacts(txn);
 			will(returnValue(Collections.singletonList(contact)));
 		}});
-		expectAddingContact(contact, true);
+		expectAddingContact(contact);
 		groupInvitationManager.createLocalState(txn);
 	}
 
@@ -167,7 +170,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 	public void testCreateLocalStateSubsequentTime() throws Exception {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 			oneOf(db).containsGroup(txn, localGroup.getId());
 			will(returnValue(true));
@@ -175,29 +178,27 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 		groupInvitationManager.createLocalState(txn);
 	}
 
-	private void expectAddingContact(Contact c, boolean contactExists)
-			throws Exception {
-		context.checking(new Expectations() {{
-			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, c);
-			will(returnValue(contactGroup));
-			oneOf(db).containsGroup(txn, contactGroup.getId());
-			will(returnValue(contactExists));
-		}});
-		if (contactExists) return;
-
+	private void expectAddingContact(Contact c) throws Exception {
 		BdfDictionary meta = BdfDictionary
 				.of(new BdfEntry(GROUP_KEY_CONTACT_ID, c.getId().getInt()));
+
 		context.checking(new Expectations() {{
+			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
+					MAJOR_VERSION, c);
+			will(returnValue(contactGroup));
 			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					CLIENT_ID, MAJOR_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, c.getId(), contactGroup.getId(),
 					SHARED);
 			oneOf(clientHelper)
 					.mergeGroupMetadata(txn, contactGroup.getId(), meta);
-			oneOf(db).getGroups(txn, PrivateGroupManager.CLIENT_ID);
+			oneOf(db).getGroups(txn, PrivateGroupManager.CLIENT_ID,
+					PrivateGroupManager.MAJOR_VERSION);
 			will(returnValue(Collections.singletonList(privateGroup)));
-			oneOf(privateGroupManager)
-					.isMember(txn, privateGroup.getId(), c.getAuthor());
+			oneOf(privateGroupManager).isMember(txn, privateGroup.getId(),
+					c.getAuthor());
 			will(returnValue(true));
 		}});
 		expectAddingMember(privateGroup.getId(), c);
@@ -206,7 +207,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 	private void expectAddingMember(GroupId g, Contact c) throws Exception {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, c);
+					MAJOR_VERSION, c);
 			will(returnValue(contactGroup));
 		}});
 		expectGetSession(noResults, new SessionId(g.getBytes()),
@@ -254,7 +255,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 
 	@Test
 	public void testAddingContact() throws Exception {
-		expectAddingContact(contact, false);
+		expectAddingContact(contact);
 		groupInvitationManager.addingContact(txn, contact);
 	}
 
@@ -262,7 +263,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 	public void testRemovingContact() throws Exception {
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(db).removeGroup(txn, contactGroup);
 		}});
@@ -477,7 +478,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 		}});
 		expectCreateStorageId();
@@ -509,7 +510,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(sessionParser)
 					.parseCreatorSession(contactGroup.getId(), bdfSession);
@@ -538,7 +539,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(db).endTransaction(txn);
 		}});
@@ -590,7 +591,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(sessionParser)
 					.parseInviteeSession(contactGroup.getId(), bdfSession);
@@ -612,7 +613,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(sessionParser)
 					.parsePeerSession(contactGroup.getId(), bdfSession);
@@ -637,7 +638,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(db).endTransaction(txn);
 		}});
@@ -676,7 +677,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContact(txn, contactId);
 			will(returnValue(contact));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(messageParser).getMessagesVisibleInUiQuery();
 			will(returnValue(query));
@@ -753,7 +754,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContacts(txn);
 			will(returnValue(Collections.singletonList(contact)));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
 					contactGroup.getId(), query);
@@ -821,10 +822,14 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 		expectGetSession(oneResult, sessionId, contactGroup.getId());
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(db).startTransaction(true);
 			will(returnValue(txn));
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					PrivateGroupManager.CLIENT_ID,
+					PrivateGroupManager.MAJOR_VERSION);
+			will(returnValue(SHARED));
 			oneOf(sessionParser)
 					.parseCreatorSession(contactGroup.getId(), bdfSession);
 			will(returnValue(creatorSession));
@@ -854,8 +859,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 		Collection<Contact> contacts =
 				Arrays.asList(contact, contact2, contact3);
 
-		Group contactGroup2 = getGroup(CLIENT_ID);
-		Group contactGroup3 = getGroup(CLIENT_ID);
+		Group contactGroup2 = getGroup(CLIENT_ID, MAJOR_VERSION);
+		Group contactGroup3 = getGroup(CLIENT_ID, MAJOR_VERSION);
 
 		MessageId storageId2 = new MessageId(getRandomId());
 		MessageId storageId3 = new MessageId(getRandomId());
@@ -874,13 +879,13 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContacts(txn);
 			will(returnValue(contacts));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact2);
+					MAJOR_VERSION, contact2);
 			will(returnValue(contactGroup2));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact3);
+					MAJOR_VERSION, contact3);
 			will(returnValue(contactGroup3));
 			// session 1
 			oneOf(sessionParser).getRole(bdfSession);
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
index 0030945428d217b1cca9b42cfc186636259373cc..f398d3a7fd4711ffd6b9cecebc206fe2930f6133 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
@@ -38,9 +38,10 @@ import static org.junit.Assert.assertTrue;
 public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final InviteeProtocolEngine engine =
-			new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new InviteeProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 	private final LocalAuthor localAuthor = getLocalAuthor();
 
 	private InviteeSession getDefaultSession(InviteeState state) {
@@ -238,6 +239,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 	@Test
 	public void testOnLeaveActionFromAccepted() throws Exception {
 		expectSendLeaveMessage(false);
+		expectSetPrivateGroupVisibility(INVISIBLE);
 		InviteeSession session = getDefaultSession(ACCEPTED);
 		InviteeSession newSession = engine.onLeaveAction(txn, session);
 
@@ -249,6 +251,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 	@Test
 	public void testOnLeaveActionFromJoined() throws Exception {
 		expectSendLeaveMessage(false);
+		expectSetPrivateGroupVisibility(INVISIBLE);
 		InviteeSession session = getDefaultSession(JOINED);
 		InviteeSession newSession = engine.onLeaveAction(txn, session);
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
index d5d53168fa57a672bcdd48e187b08b9bbb6065df..05c0920c545c3b97c83f9553ab2034cee77a8774 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
@@ -24,9 +24,10 @@ import static org.junit.Assert.assertTrue;
 public class PeerProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final PeerProtocolEngine engine =
-			new PeerProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new PeerProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 
 	private PeerSession getDefaultSession(PeerState state) {
 		return new PeerSession(contactGroupId, privateGroupId,
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java
index 37bbe16e19261f980bee6d036b07a0f50254e2c6..1fbc64387bb0bca36ed5819ec69d683286a634c8 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java
@@ -32,7 +32,7 @@ import java.util.Collection;
 import java.util.List;
 
 import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_ID;
-import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.blog.BlogSharingManager.MAJOR_VERSION;
 import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -126,7 +126,7 @@ public class BlogSharingIntegrationTest
 
 		// get sharing group and assert group message count
 		GroupId g = contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, contact1From0).getId();
+				MAJOR_VERSION, contact1From0).getId();
 		assertGroupCount(messageTracker0, g, 1, 0);
 
 		// sync first request message
@@ -201,7 +201,7 @@ public class BlogSharingIntegrationTest
 
 		// get sharing group and assert group message count
 		GroupId g = contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, contact1From0).getId();
+				MAJOR_VERSION, contact1From0).getId();
 		assertGroupCount(messageTracker0, g, 1, 0);
 
 		// sync first request message
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
index 46a9cb5e969119cce46a669750b7dc24bb03bec3..8b72b4aff33c3ee14ac063e5ac95da395a597c86 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingManagerImplTest.java
@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.versioning.ClientVersioningManager;
 import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogInvitationResponse;
@@ -24,7 +25,6 @@ import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.jmock.Expectations;
-import org.jmock.Mockery;
 import org.junit.Test;
 
 import java.util.Collection;
@@ -39,17 +39,18 @@ import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_ID;
-import static org.briarproject.briar.api.blog.BlogSharingManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.blog.BlogSharingManager.MAJOR_VERSION;
 import static org.briarproject.briar.sharing.SharingConstants.GROUP_KEY_CONTACT_ID;
 
 public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 
-	private final Mockery context = new Mockery();
 	private final BlogSharingManagerImpl blogSharingManager;
 	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
 	private final IdentityManager identityManager =
 			context.mock(IdentityManager.class);
 	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	private final SessionEncoder sessionEncoder =
 			context.mock(SessionEncoder.class);
 	private final SessionParser sessionParser =
@@ -65,11 +66,13 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 			new Contact(contactId, author, localAuthor.getId(), true, true);
 	private final Collection<Contact> contacts =
 			Collections.singletonList(contact);
-	private final Group localGroup = getGroup(CLIENT_ID);
-	private final Group contactGroup = getGroup(CLIENT_ID);
-	private final Group blogGroup = getGroup(BlogManager.CLIENT_ID);
+	private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
+	private final Group blogGroup =
+			getGroup(BlogManager.CLIENT_ID, BlogManager.MAJOR_VERSION);
 	private final Blog blog = new Blog(blogGroup, author, false);
-	private final Group localBlogGroup = getGroup(BlogManager.CLIENT_ID);
+	private final Group localBlogGroup =
+			getGroup(BlogManager.CLIENT_ID, BlogManager.MAJOR_VERSION);
 	private final Blog localBlog = new Blog(localBlogGroup, localAuthor, false);
 	@SuppressWarnings("unchecked")
 	private final ProtocolEngine<Blog> engine =
@@ -78,25 +81,26 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 	@SuppressWarnings("unchecked")
 	public BlogSharingManagerImplTest() {
 		MetadataParser metadataParser = context.mock(MetadataParser.class);
-		MessageTracker messageTracker = context.mock(MessageTracker.class);
 		MessageParser<Blog> messageParser = context.mock(MessageParser.class);
+		MessageTracker messageTracker = context.mock(MessageTracker.class);
 		InvitationFactory<Blog, BlogInvitationResponse> invitationFactory =
 				context.mock(InvitationFactory.class);
 		blogSharingManager = new BlogSharingManagerImpl(db, clientHelper,
-				metadataParser, messageParser, sessionEncoder, sessionParser,
-				messageTracker, contactGroupFactory, engine, invitationFactory,
-				identityManager, blogManager);
+				clientVersioningManager, metadataParser, messageParser,
+				sessionEncoder, sessionParser, messageTracker,
+				contactGroupFactory, engine, invitationFactory, identityManager,
+				blogManager);
 	}
 
 	@Test
-	public void testCreateLocalStateFirstTimeWithExistingContactNotSetUp()
+	public void testCreateLocalStateFirstTimeWithExistingContact()
 			throws Exception {
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
 			// The local group doesn't exist - we need to set things up
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 			oneOf(db).containsGroup(txn, localGroup.getId());
 			will(returnValue(false));
@@ -117,20 +121,14 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 		Map<MessageId, BdfDictionary> sessions = Collections.emptyMap();
 
 		context.checking(new Expectations() {{
-			// Check for contact group in BlogSharingManagerImpl
-			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
-			will(returnValue(contactGroup));
-			oneOf(db).containsGroup(txn, contactGroup.getId());
-			will(returnValue(false));
-			// Check for contact group again in SharingManagerImpl
+			// Create the contact group and share it with the contact
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
-			oneOf(db).containsGroup(txn, contactGroup.getId());
-			will(returnValue(false));
-			// Create the contact group and share it with the contact
 			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					CLIENT_ID, MAJOR_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, contactId, contactGroup.getId(),
 					SHARED);
 			// Attach the contact ID to the group
@@ -149,33 +147,6 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 		expectPreShareShareable(txn, contact, blog, sessions);
 	}
 
-	@Test
-	public void testCreateLocalStateFirstTimeWithExistingContactAlreadySetUp()
-			throws Exception {
-		Transaction txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			// The local group doesn't exist - we need to set things up
-			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
-			will(returnValue(localGroup));
-			oneOf(db).containsGroup(txn, localGroup.getId());
-			will(returnValue(false));
-			oneOf(db).addGroup(txn, localGroup);
-			// Get contacts
-			oneOf(db).getContacts(txn);
-			will(returnValue(contacts));
-			// The contact has already been set up
-			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
-			will(returnValue(contactGroup));
-			oneOf(db).containsGroup(txn, contactGroup.getId());
-			will(returnValue(true));
-		}});
-
-		blogSharingManager.createLocalState(txn);
-	}
-
 	@Test
 	public void testCreateLocalStateSubsequentTime() throws Exception {
 		Transaction txn = new Transaction(null, false);
@@ -183,7 +154,7 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 		context.checking(new Expectations() {{
 			// The local group exists - everything has been set up
 			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID,
-					CLIENT_VERSION);
+					MAJOR_VERSION);
 			will(returnValue(localGroup));
 			oneOf(db).containsGroup(txn, localGroup.getId());
 			will(returnValue(true));
@@ -225,13 +196,13 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 	private void expectPreShareShareable(Transaction txn, Contact contact,
 			Blog blog, Map<MessageId, BdfDictionary> sessions)
 			throws Exception {
-		Group contactGroup = getGroup(CLIENT_ID);
+		Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
 		BdfDictionary sessionDict = new BdfDictionary();
 		Message message = new Message(new MessageId(getRandomId()),
 				contactGroup.getId(), 42L, getRandomBytes(1337));
 		context.checking(new Expectations() {{
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(sessionParser)
 					.getSessionQuery(new SessionId(blog.getId().getBytes()));
@@ -241,6 +212,10 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 			will(returnValue(sessions));
 			if (sessions.size() == 0) {
 				oneOf(db).addGroup(txn, blog.getGroup());
+				oneOf(clientVersioningManager).getClientVisibility(txn,
+						contactId, BlogManager.CLIENT_ID,
+						BlogManager.MAJOR_VERSION);
+				will(returnValue(SHARED));
 				oneOf(db).setGroupVisibility(txn, contact.getId(),
 						blog.getGroup().getId(), SHARED);
 				oneOf(clientHelper)
@@ -265,7 +240,7 @@ public class BlogSharingManagerImplTest extends BrambleMockTestCase {
 			oneOf(db).getContacts(txn);
 			will(returnValue(contacts));
 			oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
-					CLIENT_VERSION, contact);
+					MAJOR_VERSION, contact);
 			will(returnValue(contactGroup));
 			oneOf(sessionParser)
 					.getSessionQuery(new SessionId(blog.getId().getBytes()));
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java
index bbc83660a006de207252894a6088eb52cd373308..2683ea715bdf6e17aefdfe72ad2c95cb1d7ae41d 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java
@@ -39,7 +39,7 @@ import java.util.List;
 import static junit.framework.Assert.assertNotNull;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.briarproject.briar.api.forum.ForumSharingManager.CLIENT_ID;
-import static org.briarproject.briar.api.forum.ForumSharingManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.forum.ForumSharingManager.MAJOR_VERSION;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -402,7 +402,7 @@ public class ForumSharingIntegrationTest
 
 		// response and invitation got tracked
 		Group group = contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, contact0From1);
+				MAJOR_VERSION, contact0From1);
 		assertEquals(2, c1.getMessageTracker().getGroupCount(group.getId())
 				.getMsgCount());
 
@@ -434,7 +434,7 @@ public class ForumSharingIntegrationTest
 
 		// assert that the invitation arrived
 		Group group = contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, contact0From1);
+				MAJOR_VERSION, contact0From1);
 		assertEquals(1, c1.getMessageTracker().getGroupCount(group.getId())
 				.getMsgCount());
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
index 3c2138b61392fbc77e5a67066dfb8ee5d3574269..7175cdb776d4cde3fe896e014e8110afd827d654 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
@@ -24,7 +24,7 @@ import org.briarproject.bramble.api.sync.SyncSessionFactory;
 import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.contact.ContactModule;
-import org.briarproject.bramble.crypto.CryptoModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
 import org.briarproject.bramble.properties.PropertiesModule;
@@ -32,6 +32,7 @@ import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.blog.BlogFactory;
 import org.briarproject.briar.api.blog.BlogPostFactory;
 import org.briarproject.briar.api.client.MessageTracker;
@@ -159,10 +160,9 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		deliveryWaiter = new Waiter();
 
 		startLifecycles();
-
 		getDefaultIdentities();
-		addDefaultContacts();
 		listenToEvents();
+		addDefaultContacts();
 	}
 
 	abstract protected void createComponents();
@@ -171,7 +171,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 			BriarIntegrationTestComponent component) {
 		component.inject(new BlogModule.EagerSingletons());
 		component.inject(new ContactModule.EagerSingletons());
-		component.inject(new CryptoModule.EagerSingletons());
+		component.inject(new CryptoExecutorModule.EagerSingletons());
 		component.inject(new ForumModule.EagerSingletons());
 		component.inject(new GroupInvitationModule.EagerSingletons());
 		component.inject(new IdentityModule.EagerSingletons());
@@ -184,10 +184,11 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		component.inject(new SyncModule.EagerSingletons());
 		component.inject(new SystemModule.EagerSingletons());
 		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new VersioningModule.EagerSingletons());
 	}
 
 	private void startLifecycles() throws InterruptedException {
-		// Start the lifecycle manager and wait for it to finish
+		// Start the lifecycle manager and wait for it to finish starting
 		lifecycleManager0 = c0.getLifecycleManager();
 		lifecycleManager1 = c1.getLifecycleManager();
 		lifecycleManager2 = c2.getLifecycleManager();
@@ -234,7 +235,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 		author2 = identityManager2.getLocalAuthor();
 	}
 
-	protected void addDefaultContacts() throws DbException {
+	protected void addDefaultContacts() throws Exception {
 		contactId1From0 = contactManager0
 				.addContact(author1, author0.getId(), getSecretKey(),
 						clock.currentTimeMillis(), true, true, true);
@@ -251,15 +252,28 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
 				.addContact(author0, author2.getId(), getSecretKey(),
 						clock.currentTimeMillis(), true, true, true);
 		contact0From2 = contactManager2.getContact(contactId0From2);
+
+		// Sync initial client versioning updates
+		sync0To1(1, true);
+		sync0To2(1, true);
+		sync1To0(1, true);
+		sync2To0(1, true);
+		sync0To1(1, true);
+		sync0To2(1, true);
 	}
 
-	protected void addContacts1And2() throws DbException {
+	protected void addContacts1And2() throws Exception {
 		contactId2From1 = contactManager1
 				.addContact(author2, author1.getId(), getSecretKey(),
 						clock.currentTimeMillis(), true, true, true);
 		contactId1From2 = contactManager2
 				.addContact(author1, author2.getId(), getSecretKey(),
 						clock.currentTimeMillis(), true, true, true);
+
+		// Sync initial client versioning updates
+		sync1To2(1, true);
+		sync2To1(1, true);
+		sync1To2(1, true);
 	}
 
 	@After
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index ecaf60d1b9763e069018b243312948c3906dc80f..131f92ea8b8b910ab7f49ca9f00466900fd9c15c 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.SyncSessionFactory;
 import org.briarproject.bramble.client.ClientModule;
 import org.briarproject.bramble.contact.ContactModule;
+import org.briarproject.bramble.crypto.CryptoExecutorModule;
 import org.briarproject.bramble.crypto.CryptoModule;
 import org.briarproject.bramble.data.DataModule;
 import org.briarproject.bramble.db.DatabaseModule;
@@ -24,6 +25,7 @@ import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.bramble.test.TestPluginConfigModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
 import org.briarproject.bramble.transport.TransportModule;
+import org.briarproject.bramble.versioning.VersioningModule;
 import org.briarproject.briar.api.blog.BlogFactory;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -59,6 +61,7 @@ import dagger.Component;
 		ClientModule.class,
 		ContactModule.class,
 		CryptoModule.class,
+		CryptoExecutorModule.class,
 		DataModule.class,
 		DatabaseModule.class,
 		EventModule.class,
@@ -73,7 +76,8 @@ import dagger.Component;
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		VersioningModule.class
 })
 public interface BriarIntegrationTestComponent {
 
@@ -83,7 +87,7 @@ public interface BriarIntegrationTestComponent {
 
 	void inject(ContactModule.EagerSingletons init);
 
-	void inject(CryptoModule.EagerSingletons init);
+	void inject(CryptoExecutorModule.EagerSingletons init);
 
 	void inject(ForumModule.EagerSingletons init);
 
@@ -109,6 +113,8 @@ public interface BriarIntegrationTestComponent {
 
 	void inject(TransportModule.EagerSingletons init);
 
+	void inject(VersioningModule.EagerSingletons init);
+
 	LifecycleManager getLifecycleManager();
 
 	EventBus getEventBus();