diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
index 1d807dcddb16d697b458a3288af066a1785d4615..c585b6600bb10505be3fcd751b2efea491587d04 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
@@ -1020,13 +1020,13 @@ public class ConversationActivity extends BriarActivity
 	@DatabaseExecutor
 	private void respondToForumRequest(SessionId id, boolean accept)
 			throws DbException {
-		forumSharingManager.respondToInvitation(id, accept);
+		forumSharingManager.respondToInvitation(contactId, id, accept);
 	}
 
 	@DatabaseExecutor
 	private void respondToBlogRequest(SessionId id, boolean accept)
 			throws DbException {
-		blogSharingManager.respondToInvitation(id, accept);
+		blogSharingManager.respondToInvitation(contactId, id, accept);
 	}
 
 	@DatabaseExecutor
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
index 6adad35b62b986ecf98e84d36cb9a8bd6836b5dd..39dcb24a4678430b686f1f56b374cedb982f6127 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
@@ -170,8 +170,7 @@ abstract class ConversationItem {
 			} else if (ir instanceof GroupInvitationRequest) {
 				text = ctx.getString(
 						R.string.groups_invitations_invitation_sent,
-						contactName,
-						((GroupInvitationRequest) ir).getGroupName());
+						contactName, ir.getShareable().getName());
 			} else {
 				throw new IllegalArgumentException("Unknown InvitationRequest");
 			}
@@ -194,8 +193,7 @@ abstract class ConversationItem {
 			} else if (ir instanceof GroupInvitationRequest) {
 				text = ctx.getString(
 						R.string.groups_invitations_invitation_received,
-						contactName,
-						((GroupInvitationRequest) ir).getGroupName());
+						contactName, ir.getShareable().getName());
 				type = GROUP;
 			} else {
 				throw new IllegalArgumentException("Unknown InvitationRequest");
@@ -203,7 +201,7 @@ abstract class ConversationItem {
 			return new ConversationRequestItem(ir.getId(),
 					ir.getGroupId(), type, ir.getSessionId(), text,
 					ir.getMessage(), ir.getTimestamp(), ir.isRead(),
-					ir.getInvitedGroupId(), !ir.isAvailable(),
+					ir.getShareable().getId(), !ir.isAvailable(),
 					ir.canBeOpened());
 		}
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
index 401b88ecb0ece5bbb766941e09097833f70f8f88..4f19691f7801dcc3ef2626bef7ee2ed8f1509cab 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
@@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
 import org.briarproject.briar.android.controller.handler.ExceptionHandler;
 import org.briarproject.briar.api.blog.BlogSharingManager;
+import org.briarproject.briar.api.messaging.ConversationManager;
 
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
 
 @Immutable
 @NotNullByDefault
@@ -31,14 +34,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
 	private final static Logger LOG =
 			Logger.getLogger(ShareBlogControllerImpl.class.getName());
 
+	private final ConversationManager conversationManager;
 	private final BlogSharingManager blogSharingManager;
+	private final Clock clock;
 
 	@Inject
 	ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, ContactManager contactManager,
-			BlogSharingManager blogSharingManager) {
+			ConversationManager conversationManager,
+			BlogSharingManager blogSharingManager, Clock clock) {
 		super(dbExecutor, lifecycleManager, contactManager);
+		this.conversationManager = conversationManager;
 		this.blogSharingManager = blogSharingManager;
+		this.clock = clock;
 	}
 
 	@Override
@@ -48,15 +56,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
 
 	@Override
 	public void share(final GroupId g, final Collection<ContactId> contacts,
-			final String msg,
+			final String message,
 			final ExceptionHandler<DbException> handler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				try {
+					String msg = isNullOrEmpty(message) ? null : message;
 					for (ContactId c : contacts) {
 						try {
-							blogSharingManager.sendInvitation(g, c, msg);
+							long time = Math.max(clock.currentTimeMillis(),
+									conversationManager.getGroupCount(c)
+											.getLatestMsgTime() + 1);
+							blogSharingManager.sendInvitation(g, c, msg, time);
 						} catch (NoSuchContactException | NoSuchGroupException e) {
 							if (LOG.isLoggable(WARNING))
 								LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
index 93a16307b2717a856d085901b87d353969cd17d7..9770d3bc18d292759fda57bf4a4fe4d87bf84290 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
@@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
 import org.briarproject.briar.android.controller.handler.ExceptionHandler;
 import org.briarproject.briar.api.forum.ForumSharingManager;
+import org.briarproject.briar.api.messaging.ConversationManager;
 
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
 
 @Immutable
 @NotNullByDefault
@@ -31,14 +34,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
 	private final static Logger LOG =
 			Logger.getLogger(ShareForumControllerImpl.class.getName());
 
+	private final ConversationManager conversationManager;
 	private final ForumSharingManager forumSharingManager;
+	private final Clock clock;
 
 	@Inject
 	ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, ContactManager contactManager,
-			ForumSharingManager forumSharingManager) {
+			ConversationManager conversationManager,
+			ForumSharingManager forumSharingManager, Clock clock) {
 		super(dbExecutor, lifecycleManager, contactManager);
+		this.conversationManager = conversationManager;
 		this.forumSharingManager = forumSharingManager;
+		this.clock = clock;
 	}
 
 	@Override
@@ -48,15 +56,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
 
 	@Override
 	public void share(final GroupId g, final Collection<ContactId> contacts,
-			final String msg,
+			final String message,
 			final ExceptionHandler<DbException> handler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				try {
+					String msg = isNullOrEmpty(message) ? null : message;
 					for (ContactId c : contacts) {
 						try {
-							forumSharingManager.sendInvitation(g, c, msg);
+							long time = Math.max(clock.currentTimeMillis(),
+									conversationManager.getGroupCount(c)
+											.getLatestMsgTime() + 1);
+							forumSharingManager.sendInvitation(g, c, msg, time);
 						} catch (NoSuchContactException | NoSuchGroupException e) {
 							if (LOG.isLoggable(WARNING))
 								LOG.log(WARNING, e.toString(), e);
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
index d0fabc9cb567257e5af283b1959f964b256bf358..0618f7b088b7c45c96fff92642d505f5e79c4706 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java
@@ -1,7 +1,10 @@
 package org.briarproject.briar.api.blog;
 
 import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
@@ -10,7 +13,7 @@ import org.briarproject.briar.api.sharing.InvitationRequest;
 import javax.annotation.Nullable;
 
 @NotNullByDefault
-public class BlogInvitationRequest extends InvitationRequest {
+public class BlogInvitationRequest extends InvitationRequest<Blog> {
 
 	private final String blogAuthorName;
 
@@ -19,9 +22,12 @@ public class BlogInvitationRequest extends InvitationRequest {
 			@Nullable String message, GroupId blogId,
 			boolean available, boolean canBeOpened, long time,
 			boolean local, boolean sent, boolean seen, boolean read) {
-
-		super(id, sessionId, groupId, contactId, message, blogId, available,
-				canBeOpened, time, local, sent, seen, read);
+		// TODO pass a proper blog here when redoing the BlogSharingManager
+		super(id, groupId, time, local, sent, seen, read, sessionId,
+				new Blog(new Group(blogId, BlogManager.CLIENT_ID, new byte[0]),
+						new Author(new AuthorId(new byte[AuthorId.LENGTH]),
+								blogAuthorName, new byte[0])), contactId,
+				message, available, canBeOpened);
 		this.blogAuthorName = blogAuthorName;
 	}
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
index 38a2e1680929bb1a532a111144161870868f1afb..32eaa44405fd5f69aacaa7d2db6f2a4e342d7276 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java
@@ -14,9 +14,8 @@ public class BlogInvitationResponse extends InvitationResponse {
 			GroupId groupId, ContactId contactId, GroupId blogId,
 			boolean accept, long time, boolean local, boolean sent,
 			boolean seen, boolean read) {
-
-		super(id, sessionId, groupId, contactId, blogId, accept, time, local,
-				sent, seen, read);
+		super(id, groupId, time, local, sent, seen, read, sessionId, blogId,
+				contactId, accept);
 	}
 
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
index 72c913c6f8163a4228561f2e1bc4edbefe401d69..2d23ab0e8bd768411a0b787dd8721d4076705fd8 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java
@@ -12,23 +12,18 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public class ForumInvitationRequest extends InvitationRequest {
-
-	private final String forumName;
-
-	public ForumInvitationRequest(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId, GroupId forumId,
-			String forumName, @Nullable String message, boolean available,
-			boolean canBeOpened, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
-
-		super(id, sessionId, groupId, contactId, message, forumId, available,
-				canBeOpened, time, local, sent, seen, read);
-		this.forumName = forumName;
+public class ForumInvitationRequest extends InvitationRequest<Forum> {
+
+	public ForumInvitationRequest(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, Forum forum, ContactId contactId,
+			@Nullable String message, boolean available, boolean canBeOpened) {
+		super(id, groupId, time, local, sent, seen, read, sessionId, forum,
+				contactId, message, available, canBeOpened);
 	}
 
 	public String getForumName() {
-		return forumName;
+		return getShareable().getName();
 	}
 
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
index 31eb8698ab8fff6fcd288b59b1f0e802b84915b0..48f03ce8593e4874ed550c71400b3d1df299b052 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java
@@ -13,13 +13,12 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class ForumInvitationResponse extends InvitationResponse {
 
-	public ForumInvitationResponse(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId, GroupId forumId,
-			boolean accept, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
-
-		super(id, sessionId, groupId, contactId, forumId, accept, time, local,
-				sent, seen, read);
+	public ForumInvitationResponse(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, GroupId forumId, ContactId contactId,
+			boolean accept) {
+		super(id, groupId, time, local, sent, seen, read, sessionId, forumId,
+				contactId, accept);
 	}
 
 }
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 f00a926af45f21464641057b5199c68876c72bd8..c861b4e63d72eb84c5dcae9f02e5a02353344826 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
@@ -27,6 +27,11 @@ public interface ForumManager {
 	 */
 	Forum addForum(String name) throws DbException;
 
+	/**
+	 * Subscribes to a forum within the given {@link Transaction}.
+	 */
+	void addForum(Transaction txn, Forum f) throws DbException;
+
 	/**
 	 * Unsubscribes from a forum.
 	 */
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java
index 60567907a19efa601d933126f2f187c09c07caa7..460ea3cab255423a4ff1eb9da737a09558da2f0c 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java
@@ -1,21 +1,20 @@
 package org.briarproject.briar.api.forum.event;
 
 import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.forum.ForumInvitationResponse;
 import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
 
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
 public class ForumInvitationResponseReceivedEvent extends
 		InvitationResponseReceivedEvent {
 
-	private final String forumName;
-
-	public ForumInvitationResponseReceivedEvent(String forumName,
-			ContactId contactId, ForumInvitationResponse response) {
+	public ForumInvitationResponseReceivedEvent(ContactId contactId,
+			ForumInvitationResponse response) {
 		super(contactId, response);
-		this.forumName = forumName;
 	}
 
-	public String getForumName() {
-		return forumName;
-	}
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
index 9ebdb224b6d93bdbf3d7026327917b5e126940ff..d447ae8ef176629db9ed8b25e901fd0ab4c633d2 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java
@@ -1,11 +1,11 @@
 package org.briarproject.briar.api.privategroup.invitation;
 
 import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 
 import javax.annotation.Nullable;
@@ -13,28 +13,14 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public class GroupInvitationRequest extends InvitationRequest {
-
-	private final String groupName;
-	private final Author creator;
-
-	public GroupInvitationRequest(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId, @Nullable String message,
-			GroupId privateGroupId, String groupName, Author creator,
-			boolean available, boolean canBeOpened, long time,
-			boolean local, boolean sent, boolean seen, boolean read) {
-		super(id, sessionId, groupId, contactId, message, privateGroupId,
-				available, canBeOpened, time, local, sent, seen, read);
-		this.groupName = groupName;
-		this.creator = creator;
-	}
-
-	public String getGroupName() {
-		return groupName;
-	}
-
-	public Author getCreator() {
-		return creator;
+public class GroupInvitationRequest extends InvitationRequest<PrivateGroup> {
+
+	public GroupInvitationRequest(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, PrivateGroup shareable, ContactId contactId,
+			@Nullable String message, boolean available, boolean canBeOpened) {
+		super(id, groupId, time, local, sent, seen, read, sessionId, shareable,
+				contactId, message, available, canBeOpened);
 	}
 
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
index 2f89e1e85b7e6f8ba45811e65bb93c79393f26cb..b2e7d54b538cc3ba3bb5590e5c834a654280af78 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java
@@ -13,11 +13,12 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class GroupInvitationResponse extends InvitationResponse {
 
-	public GroupInvitationResponse(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId, GroupId privateGroupId,
-			boolean accept, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
-		super(id, sessionId, groupId, contactId, privateGroupId, accept, time,
-				local, sent, seen, read);
+	public GroupInvitationResponse(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, GroupId shareableId, ContactId contactId,
+			boolean accept) {
+		super(id, groupId, time, local, sent, seen, read, sessionId,
+				shareableId, contactId, accept);
 	}
+
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java
index eab12f74142f15539b4f8f9a2dae4c5fa93a0a9f..5b4e275a3958588e71a66d725f06aef9474ac332 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java
@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.sync.GroupId;
 
+@Deprecated
 public interface InvitationFactory<I extends SharingMessage.Invitation> {
 
 	I build(GroupId groupId, BdfDictionary d) throws FormatException;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
index 7dd97d0503b8596075010540cf0059225d0f3871..4bdc394559767cdd4603ba07d39659bc81111835 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java
@@ -7,25 +7,22 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.BaseMessageHeader;
 import org.briarproject.briar.api.client.SessionId;
 
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public abstract class InvitationMessage extends BaseMessageHeader {
+public class InvitationMessage extends BaseMessageHeader {
 
 	private final SessionId sessionId;
 	private final ContactId contactId;
-	private final GroupId invitedGroupId;
 
-	public InvitationMessage(MessageId id, SessionId sessionId, GroupId groupId,
-			ContactId contactId, GroupId invitedGroupId, long time,
-			boolean local, boolean sent, boolean seen, boolean read) {
+	public InvitationMessage(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, ContactId contactId) {
 
 		super(id, groupId, time, local, read, sent, seen);
 		this.sessionId = sessionId;
 		this.contactId = contactId;
-		this.invitedGroupId = invitedGroupId;
 	}
 
 	public SessionId getSessionId() {
@@ -36,9 +33,4 @@ public abstract class InvitationMessage extends BaseMessageHeader {
 		return contactId;
 	}
 
-	@Nullable
-	public GroupId getInvitedGroupId() {
-		return invitedGroupId;
-	}
-
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
index 9acc01e7ca5893f6c0ce31c73c7cd26bbf255e64..c5239310d467872bc182c8b17c2471d7924506ee 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java
@@ -11,20 +11,19 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public abstract class InvitationRequest extends InvitationMessage {
+public class InvitationRequest<S extends Shareable> extends InvitationMessage {
 
+	private final S shareable;
 	@Nullable
 	private final String message;
 	private final boolean available, canBeOpened;
 
-	public InvitationRequest(MessageId id, SessionId sessionId, GroupId groupId,
-			ContactId contactId, @Nullable String message,
-			GroupId invitedGroupId, boolean available,
-			boolean canBeOpened, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
-		super(id, sessionId, groupId, contactId, invitedGroupId, time, local,
-				sent, seen, read);
-		if (available && canBeOpened) throw new IllegalArgumentException();
+	public InvitationRequest(MessageId id, GroupId groupId, long time,
+			boolean local, boolean sent, boolean seen, boolean read,
+			SessionId sessionId, S shareable, ContactId contactId,
+			@Nullable String message, boolean available, boolean canBeOpened) {
+		super(id, groupId, time, local, sent, seen, read, sessionId, contactId);
+		this.shareable = shareable;
 		this.message = message;
 		this.available = available;
 		this.canBeOpened = canBeOpened;
@@ -43,4 +42,8 @@ public abstract class InvitationRequest extends InvitationMessage {
 		return canBeOpened;
 	}
 
+	public S getShareable() {
+		return shareable;
+	}
+
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
index 248e443ffd2cba3528d0d2215bb22f88f9779347..517b6068e96c0bd30aaf62fdae98671a93ff1fab 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java
@@ -10,21 +10,26 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public abstract class InvitationResponse extends InvitationMessage {
+public class InvitationResponse extends InvitationMessage {
 
+	private final GroupId shareableId;
 	private final boolean accept;
 
-	public InvitationResponse(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId,
-			GroupId invitedGroupId, boolean accept, long time,
-			boolean local, boolean sent, boolean seen, boolean read) {
-
-		super(id, sessionId, groupId, contactId, invitedGroupId, time, local,
-				sent, seen, read);
+	public InvitationResponse(MessageId id, GroupId groupId,
+			long time, boolean local, boolean sent, boolean seen,
+			boolean read, SessionId sessionId, GroupId shareableId,
+			ContactId contactId, boolean accept) {
+		super(id, groupId, time, local, sent, seen, read, sessionId, contactId);
+		this.shareableId = shareableId;
 		this.accept = accept;
 	}
 
 	public boolean wasAccepted() {
 		return accept;
 	}
+
+	public GroupId getShareableId() {
+		return shareableId;
+	}
+
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java
index 53f4557e45e3ff86702d55fb0d6aba2ad6d84986..6b8fcc4a058a1fcbb1b6708a51a3ec97f108d617 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java
@@ -15,34 +15,63 @@ public interface SharingConstants {
 	 */
 	int MAX_INVITATION_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
 
+	@Deprecated
 	String CONTACT_ID = "contactId";
+	@Deprecated
 	String GROUP_ID = "groupId";
+	@Deprecated
 	String TO_BE_SHARED_BY_US = "toBeSharedByUs";
+	@Deprecated
 	String SHARED_BY_US = "sharedByUs";
+	@Deprecated
 	String SHARED_WITH_US = "sharedWithUs";
+	@Deprecated
 	String TYPE = "type";
+	@Deprecated
 	String SESSION_ID = "sessionId";
+	@Deprecated
 	String STORAGE_ID = "storageId";
+	@Deprecated
 	String STATE = "state";
+	@Deprecated
 	String LOCAL = "local";
+	@Deprecated
 	String TIME = "time";
+	@Deprecated
 	String IS_SHARER = "isSharer";
+	@Deprecated
 	String SHAREABLE_ID = "shareableId";
+	@Deprecated
 	String INVITATION_MSG = "invitationMsg";
+	@Deprecated
 	String INVITATION_ID = "invitationId";
+	@Deprecated
 	String RESPONSE_ID = "responseId";
+	@Deprecated
 	int SHARE_MSG_TYPE_INVITATION = 1;
+	@Deprecated
 	int SHARE_MSG_TYPE_ACCEPT = 2;
+	@Deprecated
 	int SHARE_MSG_TYPE_DECLINE = 3;
+	@Deprecated
 	int SHARE_MSG_TYPE_LEAVE = 4;
+	@Deprecated
 	int SHARE_MSG_TYPE_ABORT = 5;
+	@Deprecated
 	int TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US = 0;
+	@Deprecated
 	int TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US = 1;
+	@Deprecated
 	int TASK_ADD_SHARED_SHAREABLE = 2;
+	@Deprecated
 	int TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US = 3;
+	@Deprecated
 	int TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US = 4;
+	@Deprecated
 	int TASK_SHARE_SHAREABLE = 5;
+	@Deprecated
 	int TASK_UNSHARE_SHAREABLE_SHARED_BY_US = 6;
+	@Deprecated
 	int TASK_UNSHARE_SHAREABLE_SHARED_WITH_US = 7;
 
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
index 693c0f8b4204a2e3b59e65f011844f2033c4d4f7..b593d2fdf74207abe5be1f1f7216b67cfaae7154 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java
@@ -20,8 +20,8 @@ public interface SharingManager<S extends Shareable>
 	 * Sends an invitation to share the given group with the given contact
 	 * and sends an optional message along with it.
 	 */
-	void sendInvitation(GroupId groupId, ContactId contactId,
-			@Nullable String message) throws DbException;
+	void sendInvitation(GroupId shareableId, ContactId contactId,
+			@Nullable String message, long timestamp) throws DbException;
 
 	/**
 	 * Responds to a pending group invitation
@@ -32,7 +32,7 @@ public interface SharingManager<S extends Shareable>
 	/**
 	 * Responds to a pending group invitation
 	 */
-	void respondToInvitation(SessionId id, boolean accept)
+	void respondToInvitation(ContactId c, SessionId id, boolean accept)
 			throws DbException;
 
 	/**
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java
index 23340ac1c012f35a61407bfbc8db4e06279b8865..dd4b987aa95d6b3283632c147b3980f350bf00d3 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java
@@ -21,6 +21,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE
 import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
 import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
 
+@Deprecated
 @NotNullByDefault
 public interface SharingMessage {
 
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 348fe52100da5209d383b51c48e95f424c09d6a6..fef8224bdd2c01b08bc02db6531b1232bae8d463 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
@@ -108,6 +108,11 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 		return f;
 	}
 
+	@Override
+	public void addForum(Transaction txn, Forum f) throws DbException {
+		db.addGroup(txn, f.getGroup());
+	}
+
 	@Override
 	public void removeForum(Forum f) throws DbException {
 		Transaction txn = db.startTransaction(false);
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 36c00ffb1000a65e54810ac48187b43380d798af..30c1401197607a10ad93dba778f84d49860f0177 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
@@ -4,7 +4,6 @@ import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.contact.ContactId;
 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.Transaction;
@@ -177,7 +176,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 
 	void subscribeToPrivateGroup(Transaction txn, MessageId inviteId)
 			throws DbException, FormatException {
-		InviteMessage invite = getInviteMessage(txn, inviteId);
+		InviteMessage invite = messageParser.getInviteMessage(txn, inviteId);
 		PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
 				invite.getGroupName(), invite.getCreator(), invite.getSalt());
 		long timestamp =
@@ -197,14 +196,6 @@ abstract class AbstractProtocolEngine<S extends Session>
 						session.getInviteTimestamp()) + 1);
 	}
 
-	private InviteMessage getInviteMessage(Transaction txn, MessageId m)
-			throws DbException, FormatException {
-		Message message = clientHelper.getMessage(txn, m);
-		if (message == null) throw new DbException();
-		BdfList body = clientHelper.toList(message);
-		return messageParser.parseInviteMessage(message, body);
-	}
-
 	private void sendMessage(Transaction txn, Message m, MessageType type,
 			GroupId privateGroupId, boolean visibleInConversation)
 			throws DbException {
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 312957b43193ff63bed4daffbcdab91c8a472ddf..ad550742ff332c6625837bef767a7d18001e217e 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
@@ -254,8 +254,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 	private GroupInvitationResponse createInvitationResponse(
 			GroupInvitationMessage m, ContactId c, boolean accept) {
 		SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes());
-		return new GroupInvitationResponse(m.getId(), sessionId,
-				m.getContactGroupId(), c, m.getPrivateGroupId(), accept,
-				m.getTimestamp(), false, false, true, false);
+		return new GroupInvitationResponse(m.getId(), m.getContactGroupId(),
+				m.getTimestamp(), false, false, true, false, sessionId,
+				m.getPrivateGroupId(), c, accept);
 	}
 }
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 5570ffe41198db078cb978f59cd83e8a323dad26..1fac77c630aa1e049776b518c0217df1f7f0f141 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
@@ -71,7 +71,6 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	private final ProtocolEngine<CreatorSession> creatorEngine;
 	private final ProtocolEngine<InviteeSession> inviteeEngine;
 	private final ProtocolEngine<PeerSession> peerEngine;
-	private final Group localGroup;
 
 	@Inject
 	GroupInvitationManagerImpl(DatabaseComponent db,
@@ -93,12 +92,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		creatorEngine = engineFactory.createCreatorEngine();
 		inviteeEngine = engineFactory.createInviteeEngine();
 		peerEngine = engineFactory.createPeerEngine();
-		localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID);
 	}
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
-		db.addGroup(txn, localGroup);
 		// Ensure we've set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
@@ -404,22 +401,15 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			throws DbException, FormatException {
 		SessionId sessionId = getSessionId(meta.getPrivateGroupId());
 		// Look up the invite message to get the details of the private group
-		InviteMessage invite = getInviteMessage(txn, m);
+		InviteMessage invite = messageParser.getInviteMessage(txn, m);
+		PrivateGroup pg = privateGroupFactory
+				.createPrivateGroup(invite.getGroupName(), invite.getCreator(),
+						invite.getSalt());
 		boolean canBeOpened = db.containsGroup(txn, invite.getPrivateGroupId());
-		return new GroupInvitationRequest(m, sessionId, contactGroupId, c,
-				invite.getMessage(), invite.getPrivateGroupId(),
-				invite.getGroupName(), invite.getCreator(),
-				meta.isAvailableToAnswer(), canBeOpened, meta.getTimestamp(),
-				meta.isLocal(), status.isSent(), status.isSeen(),
-				meta.isRead());
-	}
-
-	private InviteMessage getInviteMessage(Transaction txn, MessageId m)
-			throws DbException, FormatException {
-		Message message = clientHelper.getMessage(txn, m);
-		if (message == null) throw new DbException();
-		BdfList body = clientHelper.toList(message);
-		return messageParser.parseInviteMessage(message, body);
+		return new GroupInvitationRequest(m, contactGroupId,
+				meta.getTimestamp(), meta.isLocal(), status.isSent(),
+				status.isSeen(), meta.isRead(), sessionId, pg, c,
+				invite.getMessage(), meta.isAvailableToAnswer(), canBeOpened);
 	}
 
 	private GroupInvitationResponse parseInvitationResponse(ContactId c,
@@ -427,10 +417,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			MessageStatus status, boolean accept)
 			throws DbException, FormatException {
 		SessionId sessionId = getSessionId(meta.getPrivateGroupId());
-		return new GroupInvitationResponse(m, sessionId, contactGroupId, c,
-				meta.getPrivateGroupId(), accept, meta.getTimestamp(),
-				meta.isLocal(), status.isSent(), status.isSeen(),
-				meta.isRead());
+		return new GroupInvitationResponse(m, contactGroupId,
+				meta.getTimestamp(), meta.isLocal(), status.isSent(),
+				status.isSeen(), meta.isRead(), sessionId,
+				meta.getPrivateGroupId(), c, accept);
 	}
 
 	@Override
@@ -481,7 +471,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 
 	private GroupInvitationItem parseGroupInvitationItem(Transaction txn,
 			Contact c, MessageId m) throws DbException, FormatException {
-		InviteMessage invite = getInviteMessage(txn, m);
+		InviteMessage invite = messageParser.getInviteMessage(txn, m);
 		PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
 				invite.getGroupName(), invite.getCreator(), invite.getSalt());
 		return new GroupInvitationItem(privateGroup, c);
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 e9dcac50860d4d7ed40bb0b04674c2716449c526..bf30f96edd34a182c3f7f3e8b7c8682427736a74 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
@@ -234,8 +234,9 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 		// Broadcast an event
 		PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
 				m.getGroupName(), m.getCreator(), m.getSalt());
-		txn.attach(new GroupInvitationRequestReceivedEvent(privateGroup,
-				contactId, createInvitationRequest(m, contactId)));
+		txn.attach(
+				new GroupInvitationRequestReceivedEvent(privateGroup, contactId,
+						createInvitationRequest(m, privateGroup, contactId)));
 		// Move to the INVITED state
 		return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
@@ -317,11 +318,11 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 	}
 
 	private GroupInvitationRequest createInvitationRequest(InviteMessage m,
-			ContactId c) {
+			PrivateGroup pg, ContactId c) {
 		SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes());
-		return new GroupInvitationRequest(m.getId(), sessionId,
-				m.getContactGroupId(), c, m.getMessage(), m.getPrivateGroupId(),
-				m.getGroupName(), m.getCreator(), true, false, m.getTimestamp(),
-				false, false, true, false);
+		return new GroupInvitationRequest(m.getId(), m.getContactGroupId(),
+				m.getTimestamp(), false, false, true, false, sessionId, pg, c,
+				m.getMessage(), true, false);
 	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java
index 9e6a7ad9dbeb423a32e526983162a46f7a829c8e..8300b305c8c8ba7d299a40694aa56f5cfa85026d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java
@@ -3,9 +3,12 @@ package org.briarproject.briar.privategroup.invitation;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfList;
+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.GroupId;
 import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
 
 @NotNullByDefault
 interface MessageParser {
@@ -18,6 +21,9 @@ interface MessageParser {
 
 	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
 
+	InviteMessage getInviteMessage(Transaction txn, MessageId m)
+			throws DbException, FormatException;
+
 	InviteMessage parseInviteMessage(Message m, BdfList body)
 			throws FormatException;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java
index b31ff7d3174240e7a1c85d1edab0f06d288fb702..1cf35ee9c32b740382678c5bdcdcb7de473c678e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java
@@ -1,9 +1,12 @@
 package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
+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.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorFactory;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -31,12 +34,14 @@ class MessageParserImpl implements MessageParser {
 
 	private final AuthorFactory authorFactory;
 	private final PrivateGroupFactory privateGroupFactory;
+	private final ClientHelper clientHelper;
 
 	@Inject
 	MessageParserImpl(AuthorFactory authorFactory,
-			PrivateGroupFactory privateGroupFactory) {
+			PrivateGroupFactory privateGroupFactory, ClientHelper clientHelper) {
 		this.authorFactory = authorFactory;
 		this.privateGroupFactory = privateGroupFactory;
+		this.clientHelper = clientHelper;
 	}
 
 	@Override
@@ -78,6 +83,15 @@ class MessageParserImpl implements MessageParser {
 				visible, available);
 	}
 
+	@Override
+	public InviteMessage getInviteMessage(Transaction txn, MessageId m)
+			throws DbException, FormatException {
+		Message message = clientHelper.getMessage(txn, m);
+		if (message == null) throw new DbException();
+		BdfList body = clientHelper.toList(message);
+		return parseInviteMessage(message, body);
+	}
+
 	@Override
 	public InviteMessage parseInviteMessage(Message m, BdfList body)
 			throws FormatException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..188c0b5013aa017336a2ace5aeb3a769844784b5
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java
@@ -0,0 +1,19 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class AbortMessage extends SharingMessage {
+
+	AbortMessage(MessageId id, GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId) {
+		super(id, contactGroupId, shareableId, timestamp, previousMessageId);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a440a5cd2bccbba67992306f523af925914c589
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java
@@ -0,0 +1,19 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class AcceptMessage extends SharingMessage {
+
+	AcceptMessage(MessageId id, @Nullable MessageId previousMessageId,
+			GroupId contactGroupId, GroupId shareableId, long timestamp) {
+		super(id, contactGroupId, shareableId, timestamp, previousMessageId);
+	}
+
+}
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 3b0c0d9846f96869c9f2c943c6dd687433d4a724..a436dd9a39bb7c3ae826fb21a3b3f52b3fd68be7 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
@@ -52,7 +52,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.RESPONSE_ID;
 @Immutable
 @NotNullByDefault
 class BlogSharingManagerImpl extends
-		SharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationRequestReceivedEvent, BlogInvitationResponseReceivedEvent>
+		OldSharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationRequestReceivedEvent, BlogInvitationResponseReceivedEvent>
 		implements BlogSharingManager, RemoveBlogHook {
 
 	private final ContactManager contactManager;
@@ -159,7 +159,7 @@ class BlogSharingManagerImpl extends
 	}
 
 	@Override
-	protected InvitationFactory<BlogInvitation, BlogSharerSessionState> getIFactory() {
+	protected OldInvitationFactory<BlogInvitation, BlogSharerSessionState> getIFactory() {
 		return iFactory;
 	}
 
@@ -251,7 +251,7 @@ class BlogSharingManagerImpl extends
 	}
 
 	private static class IFactory implements
-			InvitationFactory<BlogInvitation, BlogSharerSessionState> {
+			OldInvitationFactory<BlogInvitation, BlogSharerSessionState> {
 		@Override
 		public BlogInvitation build(GroupId groupId, BdfDictionary d)
 				throws FormatException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..65f9a9fe12d00458d506bdd75fb78e713c3693ea
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java
@@ -0,0 +1,20 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class DeclineMessage extends SharingMessage {
+
+	DeclineMessage(MessageId id, GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		super(id, contactGroupId, shareableId, timestamp, previousMessageId);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..591f25bc97af0dc93dc956e5d5943a2369d21d24
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumInvitationRequest;
+import org.briarproject.briar.api.forum.ForumInvitationResponse;
+
+import javax.inject.Inject;
+
+public class ForumInvitationFactoryImpl implements InvitationFactory<Forum> {
+
+	@Inject
+	ForumInvitationFactoryImpl() {
+	}
+
+	@Override
+	public ForumInvitationRequest createInvitationRequest(boolean local,
+			boolean sent, boolean seen, boolean read, InviteMessage<Forum> m,
+			ContactId c, boolean available, boolean canBeOpened) {
+		SessionId sessionId = new SessionId(m.getShareableId().getBytes());
+		return new ForumInvitationRequest(m.getId(), m.getContactGroupId(),
+				m.getTimestamp(), local, sent, seen, read, sessionId,
+				m.getShareable(), c, m.getMessage(), available, canBeOpened);
+	}
+
+	@Override
+	public ForumInvitationResponse createInvitationResponse(MessageId id,
+			GroupId contactGroupId, long time, boolean local, boolean sent,
+			boolean seen, boolean read, GroupId shareableId,
+			ContactId contactId, boolean accept) {
+		SessionId sessionId = new SessionId(shareableId.getBytes());
+		return new ForumInvitationResponse(id, contactGroupId, time, local,
+				sent, seen, read, sessionId, shareableId, contactId, accept);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java
deleted file mode 100644
index d6a462982fd615cafecea793c08bbe02104952c9..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.briarproject.briar.sharing;
-
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.concurrent.NotThreadSafe;
-
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
-
-@NotThreadSafe
-@NotNullByDefault
-class ForumInviteeSessionState extends InviteeSessionState {
-
-	private final String forumName;
-	private final byte[] forumSalt;
-
-	ForumInviteeSessionState(SessionId sessionId, MessageId storageId,
-			GroupId groupId, State state, ContactId contactId, GroupId forumId,
-			String forumName, byte[] forumSalt, MessageId invitationId) {
-		super(sessionId, storageId, groupId, state, contactId, forumId,
-				invitationId);
-
-		this.forumName = forumName;
-		this.forumSalt = forumSalt;
-	}
-
-	@Override
-	public BdfDictionary toBdfDictionary() {
-		BdfDictionary d = super.toBdfDictionary();
-		d.put(FORUM_NAME, getForumName());
-		d.put(FORUM_SALT, getForumSalt());
-		return d;
-	}
-
-	String getForumName() {
-		return forumName;
-	}
-
-	byte[] getForumSalt() {
-		return forumSalt;
-	}
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed1ca425cf76bd504a0421f0bf1b04cd0712de3b
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java
@@ -0,0 +1,34 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+@Immutable
+@NotNullByDefault
+class ForumMessageParserImpl extends MessageParserImpl<Forum> {
+
+	private final ForumFactory forumFactory;
+
+	@Inject
+	ForumMessageParserImpl(ClientHelper clientHelper,
+			ForumFactory forumFactory) {
+		super(clientHelper);
+		this.forumFactory = forumFactory;
+	}
+
+	@Override
+	protected Forum createShareable(BdfList descriptor)
+			throws FormatException {
+		String name = descriptor.getString(0);
+		byte[] salt = descriptor.getRaw(1);
+		return forumFactory.createForum(name, salt);
+	}
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..0dd97300b04208ef861fea585f3682dfc50377ea
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java
@@ -0,0 +1,93 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.event.Event;
+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.briar.api.client.MessageTracker;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumInvitationRequest;
+import org.briarproject.briar.api.forum.ForumInvitationResponse;
+import org.briarproject.briar.api.forum.ForumManager;
+import org.briarproject.briar.api.forum.ForumSharingManager;
+import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent;
+import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+@Immutable
+@NotNullByDefault
+class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> {
+
+	private final ForumManager forumManager;
+	private final InvitationFactory<Forum> invitationFactory;
+
+	@Inject
+	ForumProtocolEngineImpl(DatabaseComponent db,
+			ClientHelper clientHelper, MessageEncoder messageEncoder,
+			MessageParser<Forum> messageParser, MessageTracker messageTracker,
+			Clock clock, ForumManager forumManager,
+			InvitationFactory<Forum> invitationFactory) {
+		super(db, clientHelper, messageEncoder, messageParser, messageTracker,
+				clock);
+		this.forumManager = forumManager;
+		this.invitationFactory = invitationFactory;
+	}
+
+	@Override
+	Event getInvitationRequestReceivedEvent(InviteMessage<Forum> m,
+			ContactId contactId, boolean available, boolean canBeOpened) {
+		ForumInvitationRequest request =
+				(ForumInvitationRequest) invitationFactory
+						.createInvitationRequest(false, false, true, false, m,
+								contactId, available, canBeOpened);
+		return new ForumInvitationRequestReceivedEvent(m.getShareable(),
+				contactId, request);
+	}
+
+	@Override
+	Event getInvitationResponseReceivedEvent(AcceptMessage m,
+			ContactId contactId) {
+		ForumInvitationResponse response =
+				(ForumInvitationResponse) invitationFactory
+						.createInvitationResponse(m.getId(),
+								m.getContactGroupId(), m.getTimestamp(), false,
+								false, true, false, m.getShareableId(),
+								contactId, true);
+		return new ForumInvitationResponseReceivedEvent(contactId, response);
+	}
+
+	@Override
+	Event getInvitationResponseReceivedEvent(DeclineMessage m,
+			ContactId contactId) {
+		ForumInvitationResponse response =
+				(ForumInvitationResponse) invitationFactory
+						.createInvitationResponse(m.getId(),
+								m.getContactGroupId(), m.getTimestamp(), false,
+								false, true, false, m.getShareableId(),
+								contactId, true);
+		return new ForumInvitationResponseReceivedEvent(contactId, response);
+	}
+
+	@Override
+	protected ClientId getClientId() {
+		return ForumSharingManager.CLIENT_ID;
+	}
+
+	@Override
+	protected void addShareable(Transaction txn, MessageId inviteId)
+			throws DbException, FormatException {
+		InviteMessage<Forum> invite =
+				messageParser.getInviteMessage(txn, inviteId);
+		forumManager.addForum(txn, invite.getShareable());
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java
deleted file mode 100644
index 12280066ebc7727c918be700488f6c16c386f01a..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.briarproject.briar.sharing;
-
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.NotThreadSafe;
-
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
-
-@NotThreadSafe
-@NotNullByDefault
-class ForumSharerSessionState extends SharerSessionState {
-
-	private final String forumName;
-	private final byte[] forumSalt;
-
-	ForumSharerSessionState(SessionId sessionId, MessageId storageId,
-			GroupId groupId, State state, ContactId contactId, GroupId forumId,
-			String forumName, byte[] forumSalt,
-			@Nullable MessageId responseId) {
-		super(sessionId, storageId, groupId, state, contactId, forumId,
-				responseId);
-
-		this.forumName = forumName;
-		this.forumSalt = forumSalt;
-	}
-
-	@Override
-	public BdfDictionary toBdfDictionary() {
-		BdfDictionary d = super.toBdfDictionary();
-		d.put(FORUM_NAME, getForumName());
-		d.put(FORUM_SALT, getForumSalt());
-		return d;
-	}
-
-	String getForumName() {
-		return forumName;
-	}
-
-	byte[] getForumSalt() {
-		return forumSalt;
-	}
-}
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 c845f2c5f2e448b38062a5104890787e450466b5..a2e8d18d9742ab3b5db1d81b05005a8c90a7b943 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
@@ -1,78 +1,35 @@
 package org.briarproject.briar.sharing;
 
-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.ContactId;
-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.data.MetadataParser;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 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.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.MessageQueueManager;
 import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.forum.Forum;
-import org.briarproject.briar.api.forum.ForumFactory;
-import org.briarproject.briar.api.forum.ForumInvitationRequest;
-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;
-import org.briarproject.briar.api.forum.ForumSharingMessage.ForumInvitation;
-import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent;
-import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
-import org.briarproject.briar.api.sharing.InvitationMessage;
 
-import java.security.SecureRandom;
-
-import javax.annotation.Nullable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
-import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.RESPONSE_ID;
-
 @NotNullByDefault
-class ForumSharingManagerImpl extends
-		SharingManagerImpl<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState, ForumInvitationRequestReceivedEvent, ForumInvitationResponseReceivedEvent>
+class ForumSharingManagerImpl extends SharingManagerImpl<Forum>
 		implements ForumSharingManager, RemoveForumHook {
 
-	private final SFactory sFactory;
-	private final IFactory iFactory;
-	private final ISFactory isFactory;
-	private final SSFactory ssFactory;
-	private final IRFactory irFactory;
-	private final IRRFactory irrFactory;
-
 	@Inject
-	ForumSharingManagerImpl(ClientHelper clientHelper,
-			Clock clock, DatabaseComponent db,
-			ForumFactory forumFactory,
-			ForumManager forumManager,
-			MessageQueueManager messageQueueManager,
-			MetadataEncoder metadataEncoder,
-			MetadataParser metadataParser,
+	ForumSharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			MetadataParser metadataParser, MessageParser<Forum> messageParser,
+			SessionEncoder sessionEncoder, SessionParser sessionParser,
+			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
-			SecureRandom random, MessageTracker messageTracker) {
-		super(db, messageQueueManager, clientHelper, metadataParser,
-				metadataEncoder, random, contactGroupFactory, messageTracker,
-				clock);
-
-		sFactory = new SFactory(forumFactory, forumManager);
-		iFactory = new IFactory();
-		isFactory = new ISFactory();
-		ssFactory = new SSFactory();
-		irFactory = new IRFactory(sFactory);
-		irrFactory = new IRRFactory();
+			ProtocolEngine<Forum> engine,
+			InvitationFactory<Forum> invitationFactory) {
+		super(db, clientHelper, metadataParser, messageParser, sessionEncoder,
+				sessionParser, messageTracker, contactGroupFactory, engine,
+				invitationFactory);
 	}
 
 	@Override
@@ -80,221 +37,9 @@ class ForumSharingManagerImpl extends
 		return CLIENT_ID;
 	}
 
-	@Override
-	protected InvitationMessage createInvitationRequest(MessageId id,
-			ForumInvitation msg, ContactId contactId, GroupId forumId,
-			boolean available, boolean canBeOpened, long time,
-			boolean local, boolean sent, boolean seen, boolean read) {
-		return new ForumInvitationRequest(id, msg.getSessionId(),
-				msg.getGroupId(), contactId, forumId, msg.getForumName(),
-				msg.getMessage(), available, canBeOpened, time, local, sent,
-				seen, read);
-	}
-
-	@Override
-	protected InvitationMessage createInvitationResponse(MessageId id,
-			SessionId sessionId, GroupId groupId, ContactId contactId,
-			GroupId forumId, boolean accept, long time, boolean local,
-			boolean sent, boolean seen, boolean read) {
-		return new ForumInvitationResponse(id, sessionId, groupId, contactId,
-				forumId, accept, time, local, sent, seen, read);
-	}
-
-	@Override
-	protected ShareableFactory<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState> getSFactory() {
-		return sFactory;
-	}
-
-	@Override
-	protected InvitationFactory<ForumInvitation, ForumSharerSessionState> getIFactory() {
-		return iFactory;
-	}
-
-	@Override
-	protected InviteeSessionStateFactory<Forum, ForumInviteeSessionState> getISFactory() {
-		return isFactory;
-	}
-
-	@Override
-	protected SharerSessionStateFactory<Forum, ForumSharerSessionState> getSSFactory() {
-		return ssFactory;
-	}
-
-	@Override
-	protected InvitationReceivedEventFactory<ForumInviteeSessionState, ForumInvitationRequestReceivedEvent> getIRFactory() {
-		return irFactory;
-	}
-
-	@Override
-	protected InvitationResponseReceivedEventFactory<ForumSharerSessionState, ForumInvitationResponseReceivedEvent> getIRRFactory() {
-		return irrFactory;
-	}
-
 	@Override
 	public void removingForum(Transaction txn, Forum f) throws DbException {
 		removingShareable(txn, f);
 	}
 
-	private static class SFactory implements
-			ShareableFactory<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState> {
-
-		private final ForumFactory forumFactory;
-		private final ForumManager forumManager;
-
-		private SFactory(ForumFactory forumFactory, ForumManager forumManager) {
-			this.forumFactory = forumFactory;
-			this.forumManager = forumManager;
-		}
-
-		@Override
-		public BdfList encode(Forum f) {
-			return BdfList.of(f.getName(), f.getSalt());
-		}
-
-		@Override
-		public Forum get(Transaction txn, GroupId groupId)
-				throws DbException {
-			return forumManager.getForum(txn, groupId);
-		}
-
-		@Override
-		public Forum parse(BdfList shareable) throws FormatException {
-			return forumFactory
-					.createForum(shareable.getString(0), shareable.getRaw(1));
-		}
-
-		@Override
-		public Forum parse(ForumInvitation msg) {
-			return forumFactory
-					.createForum(msg.getForumName(), msg.getForumSalt());
-		}
-
-		@Override
-		public Forum parse(ForumInviteeSessionState state) {
-			return forumFactory
-					.createForum(state.getForumName(), state.getForumSalt());
-		}
-
-		@Override
-		public Forum parse(ForumSharerSessionState state) {
-			return forumFactory
-					.createForum(state.getForumName(), state.getForumSalt());
-		}
-	}
-
-	private static class IFactory implements
-			InvitationFactory<ForumInvitation, ForumSharerSessionState> {
-		@Override
-		public ForumInvitation build(GroupId groupId, BdfDictionary d)
-				throws FormatException {
-			return ForumInvitation.from(groupId, d);
-		}
-
-		@Override
-		public ForumInvitation build(ForumSharerSessionState localState,
-				long time) {
-			return new ForumInvitation(localState.getContactGroupId(),
-					localState.getSessionId(), localState.getForumName(),
-					localState.getForumSalt(), time, localState.getMessage());
-		}
-	}
-
-	private static class ISFactory implements
-			InviteeSessionStateFactory<Forum, ForumInviteeSessionState> {
-		@Override
-		public ForumInviteeSessionState build(SessionId sessionId,
-				MessageId storageId, GroupId groupId,
-				InviteeSessionState.State state, ContactId contactId,
-				GroupId forumId, BdfDictionary d) throws FormatException {
-			String forumName = d.getString(FORUM_NAME);
-			byte[] forumSalt = d.getRaw(FORUM_SALT);
-			MessageId invitationId = new MessageId(d.getRaw(INVITATION_ID));
-			return new ForumInviteeSessionState(sessionId, storageId,
-					groupId, state, contactId, forumId, forumName, forumSalt,
-					invitationId);
-		}
-
-		@Override
-		public ForumInviteeSessionState build(SessionId sessionId,
-				MessageId storageId, GroupId groupId,
-				InviteeSessionState.State state, ContactId contactId,
-				Forum forum, MessageId invitationId) {
-			return new ForumInviteeSessionState(sessionId, storageId,
-					groupId, state, contactId, forum.getId(), forum.getName(),
-					forum.getSalt(), invitationId);
-		}
-	}
-
-	private static class SSFactory implements
-			SharerSessionStateFactory<Forum, ForumSharerSessionState> {
-		@Override
-		public ForumSharerSessionState build(SessionId sessionId,
-				MessageId storageId, GroupId groupId,
-				SharerSessionState.State state, ContactId contactId,
-				GroupId forumId, BdfDictionary d) throws FormatException {
-			String forumName = d.getString(FORUM_NAME);
-			byte[] forumSalt = d.getRaw(FORUM_SALT);
-			MessageId responseId = null;
-			byte[] responseIdBytes = d.getOptionalRaw(RESPONSE_ID);
-			if (responseIdBytes != null)
-				responseId = new MessageId(responseIdBytes);
-			return new ForumSharerSessionState(sessionId, storageId,
-					groupId, state, contactId, forumId, forumName, forumSalt,
-					responseId);
-		}
-
-		@Override
-		public ForumSharerSessionState build(SessionId sessionId,
-				MessageId storageId, GroupId groupId,
-				SharerSessionState.State state, ContactId contactId,
-				Forum forum) {
-			return new ForumSharerSessionState(sessionId, storageId,
-					groupId, state, contactId, forum.getId(), forum.getName(),
-					forum.getSalt(), null);
-		}
-	}
-
-	private static class IRFactory implements
-			InvitationReceivedEventFactory<ForumInviteeSessionState, ForumInvitationRequestReceivedEvent> {
-
-		private final SFactory sFactory;
-
-		private IRFactory(SFactory sFactory) {
-			this.sFactory = sFactory;
-		}
-
-		@Override
-		public ForumInvitationRequestReceivedEvent build(
-				ForumInviteeSessionState localState, long time,
-				@Nullable String msg) {
-			Forum forum = sFactory.parse(localState);
-			ContactId contactId = localState.getContactId();
-			ForumInvitationRequest request = new ForumInvitationRequest(
-					localState.getInvitationId(), localState.getSessionId(),
-					localState.getContactGroupId(), contactId,
-					localState.getShareableId(), forum.getName(), msg, true,
-					false, time, false, false, false, false);
-			return new ForumInvitationRequestReceivedEvent(forum, contactId,
-					request);
-		}
-	}
-
-	private static class IRRFactory implements
-			InvitationResponseReceivedEventFactory<ForumSharerSessionState, ForumInvitationResponseReceivedEvent> {
-		@Override
-		public ForumInvitationResponseReceivedEvent build(
-				ForumSharerSessionState localState, boolean accept, long time) {
-			String name = localState.getForumName();
-			ContactId c = localState.getContactId();
-			MessageId responseId = localState.getResponseId();
-			if (responseId == null)
-				throw new IllegalStateException("No responseId");
-			ForumInvitationResponse response = new ForumInvitationResponse(
-					responseId, localState.getSessionId(),
-					localState.getContactGroupId(), localState.getContactId(),
-					localState.getShareableId(), accept, time, false, false,
-					false, false);
-			return new ForumInvitationResponseReceivedEvent(name, c, response);
-		}
-	}
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
index 7be98da26b1c6e09245272af845eeaee37bf00a3..14853dfb186df358fa232b8958aca2ebe3c25639 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
@@ -1,89 +1,47 @@
 package org.briarproject.briar.sharing;
 
 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.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.sync.GroupId;
 import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.client.BdfQueueMessageValidator;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
 
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.bramble.util.ValidationUtils.checkLength;
 import static org.briarproject.bramble.util.ValidationUtils.checkSize;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
 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.sharing.SharingConstants.INVITATION_MSG;
-import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
-import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
-import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
 
 @Immutable
 @NotNullByDefault
-class ForumSharingValidator extends BdfQueueMessageValidator {
+class ForumSharingValidator extends SharingValidator {
+
+	private final ForumFactory forumFactory;
 
 	@Inject
-	ForumSharingValidator(ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-		super(clientHelper, metadataEncoder, clock);
+	ForumSharingValidator(MessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock, ForumFactory forumFactory) {
+		super(messageEncoder, clientHelper, metadataEncoder, clock);
+		this.forumFactory = forumFactory;
 	}
 
 	@Override
-	protected BdfMessageContext validateMessage(Message m, Group g,
-			BdfList body) throws FormatException {
-
-		BdfDictionary d = new BdfDictionary();
-		long type = body.getLong(0);
-		byte[] id = body.getRaw(1);
-		checkLength(id, SessionId.LENGTH);
-
-		if (type == SHARE_MSG_TYPE_INVITATION) {
-			checkSize(body, 4, 5);
-
-			String name = body.getString(2);
-			checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
-
-			byte[] salt = body.getRaw(3);
-			checkLength(salt, FORUM_SALT_LENGTH);
-
-			d.put(FORUM_NAME, name);
-			d.put(FORUM_SALT, salt);
-
-			if (body.size() > 4) {
-				String msg = body.getString(4);
-				checkLength(msg, 0, MAX_INVITATION_MESSAGE_LENGTH);
-				d.put(INVITATION_MSG, msg);
-			}
-		} else {
-			checkSize(body, 2);
-			if (type != SHARE_MSG_TYPE_ACCEPT &&
-					type != SHARE_MSG_TYPE_DECLINE &&
-					type != SHARE_MSG_TYPE_LEAVE &&
-					type != SHARE_MSG_TYPE_ABORT) {
-				throw new FormatException();
-			}
-		}
-		// Return the metadata
-		d.put(TYPE, type);
-		d.put(SESSION_ID, id);
-		d.put(LOCAL, false);
-		d.put(TIME, m.getTimestamp());
-		return new BdfMessageContext(d);
+	protected GroupId validateDescriptor(BdfList descriptor)
+			throws FormatException {
+		checkSize(descriptor, 2);
+		String name = descriptor.getString(0);
+		checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
+		byte[] salt = descriptor.getRaw(1);
+		checkLength(salt, FORUM_SALT_LENGTH);
+		Forum forum = forumFactory.createForum(name, salt);
+		return forum.getId();
 	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java
index 4b55d3c6b94b42647b006b1d62e17011d61de4b5..ab9fa0e23df0ae810227d9352830cadcd5348b20 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java
@@ -1,11 +1,21 @@
 package org.briarproject.briar.sharing;
 
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.sharing.SharingMessage;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.sharing.InvitationRequest;
+import org.briarproject.briar.api.sharing.InvitationResponse;
+import org.briarproject.briar.api.sharing.Shareable;
 
-@NotNullByDefault
-interface InvitationFactory<I extends SharingMessage.Invitation, SS extends SharerSessionState>
-		extends org.briarproject.briar.api.sharing.InvitationFactory<I> {
+public interface InvitationFactory<S extends Shareable> {
+
+	InvitationRequest<S> createInvitationRequest(boolean local, boolean sent,
+			boolean seen, boolean read, InviteMessage<S> m, ContactId c,
+			boolean available, boolean canBeOpened);
+
+	InvitationResponse createInvitationResponse(MessageId id,
+			GroupId contactGroupId, long time, boolean local, boolean sent,
+			boolean seen, boolean read, GroupId shareableId,
+			ContactId contactId, boolean accept);
 
-	I build(SS localState, long time);
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
index 9c1e3f4e3c3196227800fd3a0b18d36edcdb3ffe..07d81557e680b4aaa640e1cf28a73f30f967e713 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
@@ -5,6 +5,7 @@ import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
 
 import javax.annotation.Nullable;
 
+@Deprecated
 @NotNullByDefault
 interface InvitationReceivedEventFactory<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent> {
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java
index 31ca766b2620173a4d75f593a5ec7b973971e436..2a36e36aa5547048b86988a93b85141d3e323bc2 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.sharing;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
 
+@Deprecated
 @NotNullByDefault
 interface InvitationResponseReceivedEventFactory<SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent> {
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6343cd8db2cb4656af61208fdc248cf3bd16d7b
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.sharing.Shareable;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class InviteMessage<S extends Shareable> extends SharingMessage {
+
+	private final S shareable;
+	@Nullable
+	private final String message;
+
+	InviteMessage(MessageId id, @Nullable MessageId previousMessageId,
+			GroupId contactGroupId, S shareable, @Nullable String message,
+			long timestamp) {
+		super(id, contactGroupId, shareable.getId(), timestamp,
+				previousMessageId);
+		if (message != null && message.equals(""))
+			throw new IllegalArgumentException();
+		this.shareable = shareable;
+		this.message = message;
+	}
+
+	public S getShareable() {
+		return shareable;
+	}
+
+	@Nullable
+	public String getMessage() {
+		return message;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java
index 92a534da64ccaf1e6208023b9d0157d58319200f..bdb0be196dc2589beabf6ad2a3650a4a64f07b64 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java
@@ -28,6 +28,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_S
 import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage;
 import static org.briarproject.briar.api.sharing.SharingMessage.SimpleMessage;
 
+@Deprecated
 @Immutable
 @NotNullByDefault
 class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent>
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java
index cb90748f701c6122243037e9f6fbcec1f4f05d88..efd35e5be85b6adb26ffabb864b9f7d7d333ed40 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java
@@ -23,6 +23,7 @@ import static org.briarproject.briar.sharing.InviteeSessionState.Action.LOCAL_LE
 import static org.briarproject.briar.sharing.InviteeSessionState.Action.REMOTE_INVITATION;
 import static org.briarproject.briar.sharing.InviteeSessionState.Action.REMOTE_LEAVE;
 
+@Deprecated
 @NotThreadSafe
 @NotNullByDefault
 public abstract class InviteeSessionState extends SharingSessionState {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java
index 3a0e3813e9810c1663e21c7a0dddbba0dd7ebe5f..5c827921e883a94ce0123ac08b0b8c2ec66fea79 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java
@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.sharing.Shareable;
 
+@Deprecated
 interface InviteeSessionStateFactory<S extends Shareable, IS extends InviteeSessionState> {
 
 	IS build(SessionId sessionId, MessageId storageId, GroupId groupId,
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..f518570551a286d843616f0792a76f961f03a843
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java
@@ -0,0 +1,20 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class LeaveMessage extends SharingMessage {
+
+	LeaveMessage(MessageId id, GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		super(id, contactGroupId, shareableId, timestamp, previousMessageId);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..33ce9b3fc1b160398913f7b205c89080dad838a3
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface MessageEncoder {
+
+	BdfDictionary encodeMetadata(MessageType type, GroupId shareableId,
+			long timestamp, boolean local, boolean read, boolean visible,
+			boolean available);
+
+	void setVisibleInUi(BdfDictionary meta, boolean visible);
+
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
+
+	Message encodeInviteMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, BdfList descriptor,
+			@Nullable String message);
+
+	Message encodeAcceptMessage(GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeDeclineMessage(GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeLeaveMessage(GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeAbortMessage(GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..1047df487e05596c61a603bc6973e759a2842809
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java
@@ -0,0 +1,137 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+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.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_SHAREABLE_ID;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_READ;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_VISIBLE_IN_UI;
+
+@Immutable
+@NotNullByDefault
+class MessageEncoderImpl implements MessageEncoder {
+
+	private final ClientHelper clientHelper;
+	private final MessageFactory messageFactory;
+
+	@Inject
+	MessageEncoderImpl(ClientHelper clientHelper,
+			MessageFactory messageFactory) {
+		this.clientHelper = clientHelper;
+		this.messageFactory = messageFactory;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			GroupId shareableId, long timestamp, boolean local, boolean read,
+			boolean visible, boolean available) {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
+		meta.put(MSG_KEY_SHAREABLE_ID, shareableId);
+		meta.put(MSG_KEY_TIMESTAMP, timestamp);
+		meta.put(MSG_KEY_LOCAL, local);
+		meta.put(MSG_KEY_READ, read);
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+		return meta;
+	}
+
+	@Override
+	public void setVisibleInUi(BdfDictionary meta, boolean visible) {
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+	}
+
+	@Override
+	public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+	}
+
+	@Override
+	public Message encodeInviteMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, BdfList descriptor,
+			@Nullable String message) {
+		if (message != null && message.equals(""))
+			throw new IllegalArgumentException();
+		BdfList body = BdfList.of(
+				INVITE.getValue(),
+				previousMessageId,
+				descriptor,
+				message
+		);
+		try {
+			return messageFactory.createMessage(contactGroupId, timestamp,
+					clientHelper.toByteArray(body));
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	@Override
+	public Message encodeAcceptMessage(GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(ACCEPT, contactGroupId, shareableId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeDeclineMessage(GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(DECLINE, contactGroupId, shareableId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeLeaveMessage(GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(LEAVE, contactGroupId, shareableId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeAbortMessage(GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(ABORT, contactGroupId, shareableId, timestamp,
+				previousMessageId);
+	}
+
+	private Message encodeMessage(MessageType type, GroupId contactGroupId,
+			GroupId shareableId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		BdfList body = BdfList.of(
+				type.getValue(),
+				shareableId,
+				previousMessageId
+		);
+		try {
+			return messageFactory.createMessage(contactGroupId, timestamp,
+					clientHelper.toByteArray(body));
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java
new file mode 100644
index 0000000000000000000000000000000000000000..031676249a047b696efd1ec47247637d092dec7f
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java
@@ -0,0 +1,56 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+class MessageMetadata {
+
+	private final MessageType type;
+	private final GroupId shareableId;
+	private final long timestamp;
+	private final boolean local, read, visible, available;
+
+	MessageMetadata(MessageType type, GroupId shareableId, long timestamp,
+			boolean local, boolean read, boolean visible, boolean available) {
+		this.shareableId = shareableId;
+		this.type = type;
+		this.timestamp = timestamp;
+		this.local = local;
+		this.read = read;
+		this.visible = visible;
+		this.available = available;
+	}
+
+	MessageType getMessageType() {
+		return type;
+	}
+
+	GroupId getShareableId() {
+		return shareableId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	boolean isLocal() {
+		return local;
+	}
+
+	boolean isRead() {
+		return read;
+	}
+
+	boolean isVisibleInConversation() {
+		return visible;
+	}
+
+	boolean isAvailableToAnswer() {
+		return available;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..869d017ad1a2c0427a54b5d257ad1a9815fd96bc
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java
@@ -0,0 +1,43 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+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.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.sharing.Shareable;
+
+@NotNullByDefault
+interface MessageParser<S extends Shareable> {
+
+	BdfDictionary getMessagesVisibleInUiQuery();
+
+	BdfDictionary getInvitesAvailableToAnswerQuery();
+
+	BdfDictionary getInvitesAvailableToAnswerQuery(GroupId shareableId);
+
+	MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
+
+	InviteMessage<S> getInviteMessage(Transaction txn, MessageId m)
+			throws DbException, FormatException;
+
+	InviteMessage<S> parseInviteMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AcceptMessage parseAcceptMessage(Message m, BdfList body)
+			throws FormatException;
+
+	DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException;
+
+	LeaveMessage parseLeaveMessage(Message m, BdfList body)
+			throws FormatException;
+
+	AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException;
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..4384941179563df52a39de121ef693ea38e581d8
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java
@@ -0,0 +1,139 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+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.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.sharing.Shareable;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_READ;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_SHAREABLE_ID;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_VISIBLE_IN_UI;
+
+@Immutable
+@NotNullByDefault
+abstract class MessageParserImpl<S extends Shareable>
+		implements MessageParser<S> {
+
+	private final ClientHelper clientHelper;
+
+	MessageParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getMessagesVisibleInUiQuery() {
+		return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true));
+	}
+
+	@Override
+	public BdfDictionary getInvitesAvailableToAnswerQuery() {
+		return BdfDictionary.of(
+				new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue())
+		);
+	}
+
+	@Override
+	public BdfDictionary getInvitesAvailableToAnswerQuery(GroupId shareableId) {
+		return BdfDictionary.of(
+				new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue()),
+				new BdfEntry(MSG_KEY_SHAREABLE_ID, shareableId)
+		);
+	}
+
+	@Override
+	public MessageMetadata parseMetadata(BdfDictionary meta)
+			throws FormatException {
+		MessageType type = MessageType
+				.fromValue(meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
+		GroupId shareableId = new GroupId(meta.getRaw(MSG_KEY_SHAREABLE_ID));
+		long timestamp = meta.getLong(MSG_KEY_TIMESTAMP);
+		boolean local = meta.getBoolean(MSG_KEY_LOCAL);
+		boolean read = meta.getBoolean(MSG_KEY_READ, false);
+		boolean visible = meta.getBoolean(MSG_KEY_VISIBLE_IN_UI, false);
+		boolean available = meta.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
+		return new MessageMetadata(type, shareableId, timestamp, local, read,
+				visible, available);
+	}
+
+	@Override
+	public InviteMessage<S> getInviteMessage(Transaction txn, MessageId m)
+			throws DbException, FormatException {
+		Message message = clientHelper.getMessage(txn, m);
+		if (message == null) throw new DbException();
+		BdfList body = clientHelper.toList(message);
+		return parseInviteMessage(message, body);
+	}
+
+	@Override
+	public InviteMessage<S> parseInviteMessage(Message m, BdfList body)
+			throws FormatException {
+		byte[] b = body.getOptionalRaw(1);
+		MessageId previousMessageId = (b == null ? null : new MessageId(b));
+		BdfList descriptor = body.getList(2);
+		S shareable = createShareable(descriptor);
+		String message = body.getOptionalString(3);
+		return new InviteMessage<S>(m.getId(), previousMessageId,
+				m.getGroupId(), shareable, message, m.getTimestamp());
+	}
+
+	protected abstract S createShareable(BdfList descriptor)
+			throws FormatException;
+
+	@Override
+	public AcceptMessage parseAcceptMessage(Message m, BdfList body)
+			throws FormatException {
+		GroupId shareableId = new GroupId(body.getRaw(1));
+		byte[] b = body.getOptionalRaw(2);
+		MessageId previousMessageId = (b == null ? null : new MessageId(b));
+		return new AcceptMessage(m.getId(), previousMessageId, m.getGroupId(),
+				shareableId, m.getTimestamp());
+	}
+
+	@Override
+	public DeclineMessage parseDeclineMessage(Message m, BdfList body)
+			throws FormatException {
+		GroupId shareableId = new GroupId(body.getRaw(1));
+		byte[] b = body.getOptionalRaw(2);
+		MessageId previousMessageId = (b == null ? null : new MessageId(b));
+		return new DeclineMessage(m.getId(), m.getGroupId(), shareableId,
+				m.getTimestamp(), previousMessageId);
+	}
+
+	@Override
+	public LeaveMessage parseLeaveMessage(Message m, BdfList body)
+			throws FormatException {
+		GroupId shareableId = new GroupId(body.getRaw(1));
+		byte[] b = body.getOptionalRaw(2);
+		MessageId previousMessageId = (b == null ? null : new MessageId(b));
+		return new LeaveMessage(m.getId(), m.getGroupId(), shareableId,
+				m.getTimestamp(), previousMessageId);
+	}
+
+	@Override
+	public AbortMessage parseAbortMessage(Message m, BdfList body)
+			throws FormatException {
+		GroupId shareableId = new GroupId(body.getRaw(1));
+		byte[] b = body.getOptionalRaw(2);
+		MessageId previousMessageId = (b == null ? null : new MessageId(b));
+		return new AbortMessage(m.getId(), m.getGroupId(), shareableId,
+				m.getTimestamp(), previousMessageId);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java
new file mode 100644
index 0000000000000000000000000000000000000000..1af500a745d73bd0178c44551bc8f88a7fef6e65
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java
@@ -0,0 +1,29 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MessageType {
+
+	INVITE(0), ACCEPT(1), DECLINE(2), LEAVE(3), ABORT(4);
+
+	private final int value;
+
+	MessageType(int value) {
+		this.value = value;
+	}
+
+	int getValue() {
+		return value;
+	}
+
+	static MessageType fromValue(int value) throws FormatException {
+		for (MessageType m : values()) if (m.value == value) return m;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f127a1c826251c03914e9479d10a32b74edd6b3
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java
@@ -0,0 +1,12 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.sharing.SharingMessage;
+
+@Deprecated
+@NotNullByDefault
+interface OldInvitationFactory<I extends SharingMessage.Invitation, SS extends SharerSessionState>
+		extends org.briarproject.briar.api.sharing.InvitationFactory<I> {
+
+	I build(SS localState, long time);
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..613ee0bed8aa2570a15b45ccab6c579a4df3fa5d
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java
@@ -0,0 +1,1052 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.Bytes;
+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.AddContactHook;
+import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook;
+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.data.MetadataParser;
+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.NoSuchMessageException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.event.Event;
+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.ClientId;
+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.sync.MessageStatus;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.util.StringUtils;
+import org.briarproject.briar.api.client.MessageQueueManager;
+import org.briarproject.briar.api.client.MessageTracker;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.sharing.InvitationMessage;
+import org.briarproject.briar.api.sharing.Shareable;
+import org.briarproject.briar.api.sharing.SharingInvitationItem;
+import org.briarproject.briar.api.sharing.SharingManager;
+import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
+import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
+import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
+import org.briarproject.briar.client.ConversationClientImpl;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.briar.api.client.ProtocolEngine.StateUpdate;
+import static org.briarproject.briar.api.sharing.SharingConstants.CONTACT_ID;
+import static org.briarproject.briar.api.sharing.SharingConstants.IS_SHARER;
+import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
+import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
+import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_BY_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_WITH_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.briar.api.sharing.SharingConstants.SHARING_SALT_LENGTH;
+import static org.briarproject.briar.api.sharing.SharingConstants.STATE;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHARED_SHAREABLE;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_SHARE_SHAREABLE;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_BY_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_WITH_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
+import static org.briarproject.briar.api.sharing.SharingConstants.TO_BE_SHARED_BY_US;
+import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
+import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage;
+import static org.briarproject.briar.api.sharing.SharingMessage.Invitation;
+import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
+import static org.briarproject.briar.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE;
+
+@Deprecated
+@NotNullByDefault
+abstract class OldSharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent>
+		extends ConversationClientImpl
+		implements SharingManager<S>, Client, AddContactHook,
+		RemoveContactHook {
+
+	private static final Logger LOG =
+			Logger.getLogger(OldSharingManagerImpl.class.getName());
+
+	private final MessageQueueManager messageQueueManager;
+	private final MetadataEncoder metadataEncoder;
+	private final SecureRandom random;
+	private final ContactGroupFactory contactGroupFactory;
+	private final Clock clock;
+	private final Group localGroup;
+
+	OldSharingManagerImpl(DatabaseComponent db,
+			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
+			MetadataParser metadataParser, MetadataEncoder metadataEncoder,
+			SecureRandom random, ContactGroupFactory contactGroupFactory,
+			MessageTracker messageTracker, Clock clock) {
+		super(db, clientHelper, metadataParser, messageTracker);
+		this.messageQueueManager = messageQueueManager;
+		this.metadataEncoder = metadataEncoder;
+		this.random = random;
+		this.contactGroupFactory = contactGroupFactory;
+		this.clock = clock;
+		localGroup = contactGroupFactory.createLocalGroup(getClientId());
+	}
+
+	protected abstract ClientId getClientId();
+
+	protected abstract InvitationMessage createInvitationRequest(MessageId id,
+			I msg, ContactId contactId, GroupId blogId, boolean available,
+			boolean canBeOpened, long time, boolean local, boolean sent,
+			boolean seen, boolean read);
+
+	protected abstract InvitationMessage createInvitationResponse(MessageId id,
+			SessionId sessionId, GroupId groupId, ContactId contactId,
+			GroupId blogId, boolean accept, long time, boolean local,
+			boolean sent, boolean seen, boolean read);
+
+	protected abstract ShareableFactory<S, I, IS, SS> getSFactory();
+
+	protected abstract OldInvitationFactory<I, SS> getIFactory();
+
+	protected abstract InviteeSessionStateFactory<S, IS> getISFactory();
+
+	protected abstract SharerSessionStateFactory<S, SS> getSSFactory();
+
+	protected abstract InvitationReceivedEventFactory<IS, IR> getIRFactory();
+
+	protected abstract InvitationResponseReceivedEventFactory<SS, IRR> getIRRFactory();
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+		db.addGroup(txn, localGroup);
+		// Ensure we've 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 {
+		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(CONTACT_ID, c.getId().getInt());
+			meta.put(TO_BE_SHARED_BY_US, new BdfList());
+			meta.put(SHARED_BY_US, new BdfList());
+			meta.put(SHARED_WITH_US, new BdfList());
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		// query for this contact c
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(CONTACT_ID, c.getId().getInt())
+		);
+
+		// clean up session states with that contact from localGroup
+		try {
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
+							query);
+			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
+				deleteMessage(txn, entry.getKey());
+			}
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+
+		// remove the contact group (all messages will be removed with it)
+		db.removeGroup(txn, getContactGroup(c));
+	}
+
+	@Override
+	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary d) throws DbException, FormatException {
+
+		BaseMessage msg = BaseMessage.from(getIFactory(), m.getGroupId(), d);
+		SessionId sessionId = msg.getSessionId();
+
+		if (msg.getType() == SHARE_MSG_TYPE_INVITATION) {
+			// we are an invitee who just received a new invitation
+			boolean stateExists = true;
+			try {
+				// check if we have a session with that ID already
+				getSessionState(txn, sessionId, false);
+			} catch (FormatException e) {
+				// this is what we would expect under normal circumstances
+				stateExists = false;
+			}
+			// check if we already have a state with that sessionId
+			if (stateExists) throw new FormatException();
+
+			// check if shareable can be shared
+			I invitation = (I) msg;
+			S f = getSFactory().parse(invitation);
+			ContactId contactId = getContactId(txn, m.getGroupId());
+			Contact contact = db.getContact(txn, contactId);
+			if (!canBeShared(txn, f.getId(), contact))
+				checkForRaceCondition(txn, f, contact);
+
+			// initialize state and process invitation
+			IS state = initializeInviteeState(txn, contactId, invitation,
+					m.getId());
+			InviteeEngine<IS, IR> engine =
+					new InviteeEngine<IS, IR>(getIRFactory(), clock);
+			processInviteeStateUpdate(txn, m.getId(),
+					engine.onMessageReceived(state, msg));
+			messageTracker.trackIncomingMessage(txn, m);
+		} else if (msg.getType() == SHARE_MSG_TYPE_ACCEPT ||
+				msg.getType() == SHARE_MSG_TYPE_DECLINE) {
+			// we are a sharer who just received a response
+			SS state = getSessionStateForSharer(txn, sessionId);
+			state.setResponseId(m.getId());
+			SharerEngine<I, SS, IRR> engine =
+					new SharerEngine<I, SS, IRR>(getIFactory(),
+							getIRRFactory(), clock);
+			processSharerStateUpdate(txn, m.getId(),
+					engine.onMessageReceived(state, msg));
+			messageTracker.trackIncomingMessage(txn, m);
+		} else if (msg.getType() == SHARE_MSG_TYPE_LEAVE ||
+				msg.getType() == SHARE_MSG_TYPE_ABORT) {
+			// we don't know who we are, so figure it out
+			SharingSessionState s = getSessionState(txn, sessionId, true);
+			if (s instanceof SharerSessionState) {
+				// we are a sharer and the invitee wants to leave or abort
+				SS state = (SS) s;
+				SharerEngine<I, SS, IRR> engine =
+						new SharerEngine<I, SS, IRR>(getIFactory(),
+								getIRRFactory(), clock);
+				processSharerStateUpdate(txn, m.getId(),
+						engine.onMessageReceived(state, msg));
+			} else {
+				// we are an invitee and the sharer wants to leave or abort
+				IS state = (IS) s;
+				InviteeEngine<IS, IR> engine =
+						new InviteeEngine<IS, IR>(getIRFactory(), clock);
+				processInviteeStateUpdate(txn, m.getId(),
+						engine.onMessageReceived(state, msg));
+			}
+		} else {
+			// message has passed validator, so that should never happen
+			throw new AssertionError("Illegal Sharing Message");
+		}
+		// don't share message as other party already has it
+		return false;
+	}
+
+	@Override
+	public void sendInvitation(GroupId groupId, ContactId contactId,
+			@Nullable String msg, long timestamp) throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			// initialize local state for sharer
+			S f = getSFactory().get(txn, groupId);
+			SS localState = initializeSharerState(txn, f, contactId);
+
+			// add invitation message to local state to be available for engine
+			if (!StringUtils.isNullOrEmpty(msg)) {
+				int msgLength = StringUtils.toUtf8(msg).length;
+				if (msgLength > MAX_INVITATION_MESSAGE_LENGTH)
+					throw new IllegalArgumentException();
+				localState.setMessage(msg);
+			}
+
+			// start engine and process its state update
+			SharerEngine<I, SS, IRR> engine =
+					new SharerEngine<I, SS, IRR>(getIFactory(),
+							getIRRFactory(), clock);
+			StateUpdate<SS, BaseMessage> update =
+					engine.onLocalAction(localState,
+							SharerSessionState.Action.LOCAL_INVITATION);
+			processSharerStateUpdate(txn, null, update);
+
+			// track message
+			// TODO handle this properly without engine hacks (#376)
+			long time = update.toSend.get(0).getTime();
+			messageTracker.trackMessage(txn, localState.getContactGroupId(), time,
+					true);
+
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException();
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public void respondToInvitation(S f, Contact c, boolean accept)
+			throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			// find session state based on shareable
+			IS localState = getSessionStateForResponse(txn, f, c);
+			respondToInvitation(txn, localState, accept);
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public void respondToInvitation(ContactId c, SessionId id, boolean accept)
+			throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			IS localState = (IS) getSessionState(txn, id, true);
+			respondToInvitation(txn, localState, accept);
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private void respondToInvitation(Transaction txn, IS localState,
+			boolean accept) throws DbException, FormatException {
+		// define action
+		InviteeSessionState.Action localAction;
+		if (accept) {
+			localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
+		} else {
+			localAction = InviteeSessionState.Action.LOCAL_DECLINE;
+		}
+
+		// start engine and process its state update
+		InviteeEngine<IS, IR> engine =
+				new InviteeEngine<IS, IR>(getIRFactory(), clock);
+		StateUpdate<IS, BaseMessage> update =
+				engine.onLocalAction(localState, localAction);
+		processInviteeStateUpdate(txn, null, update);
+
+		// track message
+		// TODO handle this properly without engine hacks (#376)
+		long time = update.toSend.get(0).getTime();
+		messageTracker.trackMessage(txn, localState.getContactGroupId(), time, true);
+	}
+
+	@Override
+	public Collection<InvitationMessage> getInvitationMessages(
+			ContactId contactId)
+			throws DbException {
+
+		Transaction txn = db.startTransaction(true);
+		try {
+			Contact contact = db.getContact(txn, contactId);
+			Group group = getContactGroup(contact);
+
+			Collection<InvitationMessage> list =
+					new ArrayList<InvitationMessage>();
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, group.getId());
+			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+				BdfDictionary d = m.getValue();
+				long type = d.getLong(TYPE);
+				if (type == SHARE_MSG_TYPE_LEAVE ||
+						type == SHARE_MSG_TYPE_ABORT) continue;
+				try {
+					MessageStatus status =
+							db.getMessageStatus(txn, contactId, m.getKey());
+					SharingSessionState s;
+					long time = d.getLong(TIME);
+					boolean local = d.getBoolean(LOCAL);
+					boolean read = d.getBoolean(MSG_KEY_READ, false);
+					boolean available = false, canBeOpened = false;
+
+					if (type == SHARE_MSG_TYPE_INVITATION) {
+						I msg = getIFactory().build(group.getId(), d);
+						SessionId sessionId = msg.getSessionId();
+						s = getSessionState(txn, sessionId, true);
+						if (!local) {
+							// figure out whether the shareable is still available
+							if (!(s instanceof InviteeSessionState))
+								continue;
+							available = ((InviteeSessionState) s).getState() ==
+									AWAIT_LOCAL_RESPONSE;
+							if (!available) {
+								canBeOpened = db.containsGroup(txn,
+										s.getShareableId());
+							}
+						}
+						InvitationMessage im =
+								createInvitationRequest(m.getKey(), msg,
+										contactId, s.getShareableId(),
+										available, canBeOpened, time, local,
+										status.isSent(), status.isSeen(), read);
+						list.add(im);
+					} else if (type == SHARE_MSG_TYPE_ACCEPT ||
+							type == SHARE_MSG_TYPE_DECLINE) {
+						boolean accept = type == SHARE_MSG_TYPE_ACCEPT;
+						BaseMessage msg = BaseMessage
+								.from(getIFactory(), group.getId(), d);
+						SessionId sessionId = msg.getSessionId();
+						s = getSessionState(txn, sessionId, true);
+						InvitationMessage im =
+								createInvitationResponse(m.getKey(), sessionId,
+										group.getId(), contactId,
+										s.getShareableId(), accept, time, local,
+										status.isSent(), status.isSeen(), read);
+						list.add(im);
+					} else {
+						throw new RuntimeException("Unexpected Message Type");
+					}
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+			db.commitTransaction(txn);
+			return list;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public Collection<SharingInvitationItem> getInvitations()
+			throws DbException {
+		List<SharingInvitationItem> invitations =
+				new ArrayList<SharingInvitationItem>();
+		Transaction txn = db.startTransaction(true);
+		try {
+			Set<S> shareables = new HashSet<S>();
+			Map<GroupId, Collection<Contact>> newSharers =
+					new HashMap<GroupId, Collection<Contact>>();
+			Collection<Contact> contacts = db.getContacts(txn);
+
+			// get invitations from each contact
+			for (Contact contact : contacts) {
+				Collection<S> newShareables = getInvited(txn, contact);
+				shareables.addAll(newShareables);
+				for (S s : newShareables) {
+					if (newSharers.containsKey(s.getId())) {
+						newSharers.get(s.getId()).add(contact);
+					} else {
+						Collection<Contact> c = new ArrayList<Contact>();
+						c.add(contact);
+						newSharers.put(s.getId(), c);
+					}
+				}
+			}
+			// construct InvitationItem objects
+			for (S s : shareables) {
+				Collection<Contact> newS = newSharers.get(s.getId());
+				boolean subscribed = db.containsGroup(txn, s.getId());
+				SharingInvitationItem invitation =
+						new SharingInvitationItem(s, subscribed, newS);
+				invitations.add(invitation);
+			}
+			db.commitTransaction(txn);
+			return invitations;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private Collection<S> getInvited(Transaction txn, Contact contact)
+			throws DbException, FormatException {
+
+		// query for all external invitations
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(TYPE, SHARE_MSG_TYPE_INVITATION),
+				new BdfEntry(LOCAL, false)
+		);
+		Group group = getContactGroup(contact);
+
+		Set<S> invited = new HashSet<S>();
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, group.getId(), query);
+		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+			BdfDictionary d = m.getValue();
+			try {
+				I msg = getIFactory().build(group.getId(), d);
+				IS iss = (IS) getSessionState(txn, msg.getSessionId(), true);
+				// get and add the shareable if the invitation is unanswered
+				if (iss.getState().equals(AWAIT_LOCAL_RESPONSE)) {
+					S s = getSFactory().parse(iss);
+					invited.add(s);
+				}
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, e.toString(), e);
+			}
+		}
+		return invited;
+	}
+
+	@Override
+	public Collection<Contact> getSharedWith(GroupId g) throws DbException {
+		try {
+			List<Contact> shared = new ArrayList<Contact>();
+			Transaction txn = db.startTransaction(true);
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					GroupId contactGroup = getContactGroup(c).getId();
+					if (listContains(txn, contactGroup, g, SHARED_BY_US))
+						shared.add(c);
+					else if (listContains(txn, contactGroup, g, SHARED_WITH_US))
+						shared.add(c);
+				}
+				db.commitTransaction(txn);
+			} finally {
+				db.endTransaction(txn);
+			}
+			return shared;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public boolean canBeShared(GroupId g, Contact c) throws DbException {
+		boolean canBeShared;
+		Transaction txn = db.startTransaction(true);
+		try {
+			canBeShared = canBeShared(txn, g, c);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return canBeShared;
+	}
+
+	protected boolean canBeShared(Transaction txn, GroupId g, Contact c)
+			throws DbException {
+
+		try {
+			GroupId contactGroup = getContactGroup(c).getId();
+			return !listContains(txn, contactGroup, g, SHARED_BY_US) &&
+					!listContains(txn, contactGroup, g, SHARED_WITH_US) &&
+					!listContains(txn, contactGroup, g, TO_BE_SHARED_BY_US);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	void removingShareable(Transaction txn, S f) throws DbException {
+		try {
+			for (Contact c : db.getContacts(txn)) {
+				GroupId g = getContactGroup(c).getId();
+				if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) {
+					leaveShareable(txn, c.getId(), f);
+				}
+				if (removeFromList(txn, g, SHARED_BY_US, f)) {
+					leaveShareable(txn, c.getId(), f);
+				}
+				if (removeFromList(txn, g, SHARED_WITH_US, f)) {
+					leaveShareable(txn, c.getId(), f);
+				}
+			}
+		} catch (IOException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private void checkForRaceCondition(Transaction txn, S f, Contact c)
+			throws FormatException, DbException {
+
+		GroupId contactGroup = getContactGroup(c).getId();
+		if (!listContains(txn, contactGroup, f.getId(), TO_BE_SHARED_BY_US))
+			// no race-condition, this invitation is invalid
+			throw new FormatException();
+
+		// we have an invitation race condition
+		LocalAuthor author = db.getLocalAuthor(txn, c.getLocalAuthorId());
+		Bytes ourKey = new Bytes(author.getPublicKey());
+		Bytes theirKey = new Bytes(c.getAuthor().getPublicKey());
+
+		// determine which invitation takes precedence
+		boolean alice = ourKey.compareTo(theirKey) < 0;
+
+		if (alice) {
+			// our own invitation takes precedence, so just delete Bob's
+			LOG.info(
+					"Invitation race-condition: We are Alice deleting Bob's invitation.");
+			throw new FormatException();
+		} else {
+			// we are Bob, so we need to "take back" our own invitation
+			LOG.info(
+					"Invitation race-condition: We are Bob taking back our invitation.");
+			SharingSessionState state =
+					getSessionStateForLeaving(txn, f, c.getId());
+			if (state instanceof SharerSessionState) {
+				//SharerEngine engine = new SharerEngine();
+				//processSharerStateUpdate(txn, null,
+				//		engine.onLocalAction((SharerSessionState) state,
+				//				Action.LOCAL_LEAVE));
+
+				// simply remove from list instead of involving engine
+				removeFromList(txn, contactGroup, TO_BE_SHARED_BY_US, f);
+				// TODO here we could also remove the old session state
+				//      and invitation message
+			}
+		}
+
+	}
+
+	private SS initializeSharerState(Transaction txn, S f,
+			ContactId contactId) throws FormatException, DbException {
+
+		Contact c = db.getContact(txn, contactId);
+		Group group = getContactGroup(c);
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes salt = new Bytes(new byte[SHARING_SALT_LENGTH]);
+		random.nextBytes(salt.getBytes());
+		Message m = clientHelper.createMessage(localGroup.getId(), now,
+				BdfList.of(salt));
+		SessionId sessionId = new SessionId(m.getId().getBytes());
+
+		SS s = getSSFactory().build(sessionId, m.getId(), group.getId(),
+				SharerSessionState.State.PREPARE_INVITATION, contactId, f);
+
+		// save local state to database
+		BdfDictionary d = s.toBdfDictionary();
+		clientHelper.addLocalMessage(txn, m, d, false);
+
+		return s;
+	}
+
+	private IS initializeInviteeState(Transaction txn,
+			ContactId contactId, I msg, MessageId id)
+			throws FormatException, DbException {
+
+		Contact c = db.getContact(txn, contactId);
+		Group group = getContactGroup(c);
+		S f = getSFactory().parse(msg);
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes mSalt = new Bytes(new byte[SHARING_SALT_LENGTH]);
+		random.nextBytes(mSalt.getBytes());
+		Message m = clientHelper.createMessage(localGroup.getId(), now,
+				BdfList.of(mSalt));
+
+		IS s = getISFactory()
+				.build(msg.getSessionId(), m.getId(), group.getId(),
+						InviteeSessionState.State.AWAIT_INVITATION, contactId,
+						f, id);
+
+		// save local state to database
+		BdfDictionary d = s.toBdfDictionary();
+		clientHelper.addLocalMessage(txn, m, d, false);
+
+		return s;
+	}
+
+	private SharingSessionState getSessionState(Transaction txn,
+			SessionId sessionId, boolean warn)
+			throws DbException, FormatException {
+
+		try {
+			return getSessionStateForSharer(txn, sessionId);
+		} catch (NoSuchMessageException e) {
+			// State not found directly, so query for state for invitee
+			BdfDictionary query = BdfDictionary.of(
+					new BdfEntry(SESSION_ID, sessionId)
+			);
+
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
+							query);
+
+			if (map.size() > 1 && LOG.isLoggable(WARNING)) {
+				LOG.warning(
+						"More than one session state found for message with session ID " +
+								Arrays.hashCode(sessionId.getBytes()));
+			}
+			if (map.isEmpty()) {
+				if (warn && LOG.isLoggable(WARNING)) {
+					LOG.warning(
+							"No session state found for message with session ID " +
+									Arrays.hashCode(sessionId.getBytes()));
+				}
+				throw new FormatException();
+			}
+			return SharingSessionState
+					.fromBdfDictionary(getISFactory(), getSSFactory(),
+							map.values().iterator().next());
+		}
+	}
+
+	private SS getSessionStateForSharer(Transaction txn,
+			SessionId sessionId)
+			throws DbException, FormatException {
+
+		// we should be able to get the sharer state directly from sessionId
+		MessageId storageId = new MessageId(sessionId.getBytes());
+		BdfDictionary d =
+				clientHelper.getMessageMetadataAsDictionary(txn, storageId);
+
+		if (!d.getBoolean(IS_SHARER)) throw new FormatException();
+
+		return (SS) SharingSessionState
+				.fromBdfDictionary(getISFactory(), getSSFactory(), d);
+	}
+
+	private IS getSessionStateForResponse(Transaction txn,
+			S f, Contact c) throws DbException, FormatException {
+
+		// query for invitee states for that shareable in state await response
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(IS_SHARER, false),
+				new BdfEntry(CONTACT_ID, c.getId().getInt()),
+				new BdfEntry(SHAREABLE_ID, f.getId()),
+				new BdfEntry(STATE, AWAIT_LOCAL_RESPONSE.getValue())
+		);
+
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
+
+		if (map.size() > 1 && LOG.isLoggable(WARNING)) {
+			LOG.warning(
+					"More than one session state found for shareable with ID " +
+							Arrays.hashCode(f.getId().getBytes()) +
+							" in state AWAIT_LOCAL_RESPONSE for contact " +
+							c.getAuthor().getName());
+		}
+		if (map.isEmpty()) {
+			if (LOG.isLoggable(WARNING)) {
+				LOG.warning(
+						"No session state found for shareable with ID " +
+								Arrays.hashCode(f.getId().getBytes()) +
+								" in state AWAIT_LOCAL_RESPONSE");
+			}
+			throw new DbException();
+		}
+		return (IS) SharingSessionState
+				.fromBdfDictionary(getISFactory(), getSSFactory(),
+						map.values().iterator().next());
+	}
+
+	private SharingSessionState getSessionStateForLeaving(Transaction txn,
+			S f, ContactId c) throws DbException, FormatException {
+
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(CONTACT_ID, c.getInt()),
+				new BdfEntry(SHAREABLE_ID, f.getId())
+		);
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
+		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+			BdfDictionary d = m.getValue();
+			try {
+				SharingSessionState s = SharingSessionState
+						.fromBdfDictionary(getISFactory(), getSSFactory(), d);
+
+				// check that a shareable get be left in current session
+				if (s instanceof SharerSessionState) {
+					SharerSessionState state = (SharerSessionState) s;
+					SharerSessionState.State nextState =
+							state.getState()
+									.next(SharerSessionState.Action.LOCAL_LEAVE);
+					if (nextState != SharerSessionState.State.ERROR) {
+						return state;
+					}
+				} else {
+					InviteeSessionState state = (InviteeSessionState) s;
+					InviteeSessionState.State nextState = state.getState()
+							.next(InviteeSessionState.Action.LOCAL_LEAVE);
+					if (nextState != InviteeSessionState.State.ERROR) {
+						return state;
+					}
+				}
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			}
+		}
+		throw new FormatException();
+	}
+
+	private void processStateUpdate(Transaction txn,
+			@Nullable MessageId messageId,
+			StateUpdate<SharingSessionState, BaseMessage> result, S f)
+			throws DbException, FormatException {
+
+		// perform actions based on new local state
+		performTasks(txn, result.localState, f);
+
+		// save new local state
+		MessageId storageId = result.localState.getStorageId();
+		clientHelper.mergeMessageMetadata(txn, storageId,
+				result.localState.toBdfDictionary());
+
+		// send messages
+		for (BaseMessage msg : result.toSend) {
+			sendMessage(txn, msg);
+		}
+
+		// broadcast events
+		for (Event event : result.toBroadcast) {
+			txn.attach(event);
+		}
+
+		// delete message
+		if (result.deleteMessage && messageId != null) {
+			if (LOG.isLoggable(INFO)) {
+				LOG.info("Deleting message with id " + messageId.hashCode());
+			}
+			db.deleteMessage(txn, messageId);
+			db.deleteMessageMetadata(txn, messageId);
+		}
+	}
+
+	private void processSharerStateUpdate(Transaction txn,
+			@Nullable MessageId messageId, StateUpdate<SS, BaseMessage> result)
+			throws DbException, FormatException {
+
+		StateUpdate<SharingSessionState, BaseMessage> r =
+				new StateUpdate<SharingSessionState, BaseMessage>(
+						result.deleteMessage, result.deleteState,
+						result.localState, result.toSend, result.toBroadcast);
+
+		// get shareable for later
+		S f = getSFactory().parse(result.localState);
+
+		processStateUpdate(txn, messageId, r, f);
+	}
+
+	private void processInviteeStateUpdate(Transaction txn,
+			@Nullable MessageId messageId, StateUpdate<IS, BaseMessage> result)
+			throws DbException, FormatException {
+
+		StateUpdate<SharingSessionState, BaseMessage> r =
+				new StateUpdate<SharingSessionState, BaseMessage>(
+						result.deleteMessage, result.deleteState,
+						result.localState, result.toSend, result.toBroadcast);
+
+		// get shareable for later
+		S f = getSFactory().parse(result.localState);
+
+		processStateUpdate(txn, messageId, r, f);
+	}
+
+	private void performTasks(Transaction txn, SharingSessionState localState,
+			S f) throws FormatException, DbException {
+
+		if (localState.getTask() == -1) return;
+
+		// remember task and remove it from localState
+		long task = localState.getTask();
+		localState.setTask(-1);
+
+		// get group ID for later
+		GroupId groupId = localState.getContactGroupId();
+		// get contact ID for later
+		ContactId contactId = localState.getContactId();
+
+		// perform tasks
+		if (task == TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US) {
+			addToList(txn, groupId, SHARED_WITH_US, f);
+		} else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US) {
+			removeFromList(txn, groupId, SHARED_WITH_US, f);
+		} else if (task == TASK_ADD_SHARED_SHAREABLE) {
+			// TODO we might want to call the add() method of the respective
+			//      manager here, because blogs add a description for example
+			db.addGroup(txn, f.getGroup());
+			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
+		} else if (task == TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US) {
+			addToList(txn, groupId, TO_BE_SHARED_BY_US, f);
+		} else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US) {
+			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
+		} else if (task == TASK_SHARE_SHAREABLE) {
+			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
+			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
+			addToList(txn, groupId, SHARED_BY_US, f);
+		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) {
+			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
+			removeFromList(txn, groupId, SHARED_BY_US, f);
+			// broadcast event informing UI that contact has left the group
+			ContactLeftShareableEvent
+					e = new ContactLeftShareableEvent(f.getId(), contactId);
+			txn.attach(e);
+		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) {
+			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
+			removeFromList(txn, groupId, SHARED_WITH_US, f);
+			// broadcast event informing UI that contact has left the group
+			ContactLeftShareableEvent
+					e = new ContactLeftShareableEvent(f.getId(), contactId);
+			txn.attach(e);
+		}
+	}
+
+	private void sendMessage(Transaction txn, BaseMessage m)
+			throws FormatException, DbException {
+
+		byte[] body = clientHelper.toByteArray(m.toBdfList());
+		Group group = db.getGroup(txn, m.getGroupId());
+
+		// add message itself as metadata
+		BdfDictionary d = m.toBdfDictionary();
+		d.put(LOCAL, true);
+		d.put(TIME, m.getTime());
+		Metadata meta = metadataEncoder.encode(d);
+
+		messageQueueManager
+				.sendMessage(txn, group, m.getTime(), body, meta);
+	}
+
+	@Override
+	public Group getContactGroup(Contact c) {
+		return contactGroupFactory.createContactGroup(getClientId(), c);
+	}
+
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
+				contactGroupId);
+		return new ContactId(meta.getLong(CONTACT_ID).intValue());
+	}
+
+	private void leaveShareable(Transaction txn, ContactId c, S f)
+			throws DbException, FormatException {
+
+		SharingSessionState state = getSessionStateForLeaving(txn, f, c);
+		if (state instanceof SharerSessionState) {
+			SharerSessionState.Action action =
+					SharerSessionState.Action.LOCAL_LEAVE;
+			SharerEngine<I, SS, IRR> engine =
+					new SharerEngine<I, SS, IRR>(getIFactory(),
+							getIRRFactory(), clock);
+			processSharerStateUpdate(txn, null,
+					engine.onLocalAction((SS) state, action));
+		} else {
+			InviteeSessionState.Action action =
+					InviteeSessionState.Action.LOCAL_LEAVE;
+			InviteeEngine<IS, IR> engine =
+					new InviteeEngine<IS, IR>(getIRFactory(), clock);
+			processInviteeStateUpdate(txn, null,
+					engine.onLocalAction((IS) state, action));
+		}
+	}
+
+	private boolean listContains(Transaction txn, GroupId contactGroup,
+			GroupId shareable, String key) throws DbException, FormatException {
+
+		List<S> list = getShareableList(txn, contactGroup, key);
+		for (S f : list) {
+			if (f.getId().equals(shareable)) return true;
+		}
+		return false;
+	}
+
+	private boolean addToList(Transaction txn, GroupId groupId, String key,
+			S f) throws DbException, FormatException {
+
+		List<S> shareables = getShareableList(txn, groupId, key);
+		if (shareables.contains(f)) return false;
+		shareables.add(f);
+		storeShareableList(txn, groupId, key, shareables);
+		return true;
+	}
+
+	private boolean removeFromList(Transaction txn, GroupId groupId, String key,
+			S f) throws DbException, FormatException {
+
+		List<S> shareables = getShareableList(txn, groupId, key);
+		if (shareables.remove(f)) {
+			storeShareableList(txn, groupId, key, shareables);
+			return true;
+		}
+		return false;
+	}
+
+	private List<S> getShareableList(Transaction txn, GroupId groupId,
+			String key) throws DbException, FormatException {
+
+		BdfDictionary metadata =
+				clientHelper.getGroupMetadataAsDictionary(txn, groupId);
+		BdfList list = metadata.getList(key);
+
+		return parseShareableList(list);
+	}
+
+	private void storeShareableList(Transaction txn, GroupId groupId,
+			String key,
+			List<S> shareables) throws DbException, FormatException {
+
+		BdfList list = encodeShareableList(shareables);
+		BdfDictionary metadata = BdfDictionary.of(
+				new BdfEntry(key, list)
+		);
+		clientHelper.mergeGroupMetadata(txn, groupId, metadata);
+	}
+
+	private BdfList encodeShareableList(List<S> shareables) {
+		BdfList shareableList = new BdfList();
+		for (S f : shareables)
+			shareableList.add(getSFactory().encode(f));
+		return shareableList;
+	}
+
+	private List<S> parseShareableList(BdfList list) throws FormatException {
+		List<S> shareables = new ArrayList<S>(list.size());
+		for (int i = 0; i < list.size(); i++) {
+			BdfList shareable = list.getList(i);
+			shareables.add(getSFactory().parse(shareable));
+		}
+		return shareables;
+	}
+
+	private void deleteMessage(Transaction txn, MessageId messageId)
+			throws DbException {
+
+		if (LOG.isLoggable(INFO))
+			LOG.info("Deleting message with ID: " + messageId.hashCode());
+
+		db.deleteMessage(txn, messageId);
+		db.deleteMessageMetadata(txn, messageId);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..4277303c628cdaf5581e325325ca3f82a7e98fcb
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.sharing.Shareable;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface ProtocolEngine<S extends Shareable> {
+
+	Session onInviteAction(Transaction txn, Session session,
+			@Nullable String message, long timestamp) throws DbException;
+
+	Session onAcceptAction(Transaction txn, Session session) throws DbException;
+
+	Session onDeclineAction(Transaction txn, Session session)
+			throws DbException;
+
+	Session onLeaveAction(Transaction txn, Session session) throws DbException;
+
+	Session onInviteMessage(Transaction txn, Session session,
+			InviteMessage<S> m) throws DbException, FormatException;
+
+	Session onAcceptMessage(Transaction txn, Session session, AcceptMessage m)
+			throws DbException, FormatException;
+
+	Session onDeclineMessage(Transaction txn, Session session, DeclineMessage m)
+			throws DbException, FormatException;
+
+	Session onLeaveMessage(Transaction txn, Session session, LeaveMessage m)
+			throws DbException, FormatException;
+
+	Session onAbortMessage(Transaction txn, Session session, AbortMessage m)
+			throws DbException, FormatException;
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..dd8132d90aec63b30b489e3e3f55934697be584c
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java
@@ -0,0 +1,619 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactId;
+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.Transaction;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+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.system.Clock;
+import org.briarproject.briar.api.client.MessageTracker;
+import org.briarproject.briar.api.client.ProtocolStateException;
+import org.briarproject.briar.api.sharing.Shareable;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+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;
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
+import static org.briarproject.briar.sharing.SharingConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.briar.sharing.State.ERROR;
+import static org.briarproject.briar.sharing.State.LOCAL_INVITED;
+import static org.briarproject.briar.sharing.State.LOCAL_LEFT;
+import static org.briarproject.briar.sharing.State.REMOTE_HANGING;
+import static org.briarproject.briar.sharing.State.REMOTE_INVITED;
+import static org.briarproject.briar.sharing.State.REMOTE_LEFT;
+import static org.briarproject.briar.sharing.State.SHARING;
+import static org.briarproject.briar.sharing.State.START;
+
+@Immutable
+@NotNullByDefault
+abstract class ProtocolEngineImpl<S extends Shareable>
+		implements ProtocolEngine<S> {
+
+	protected final DatabaseComponent db;
+	protected final ClientHelper clientHelper;
+	protected final MessageParser<S> messageParser;
+
+	private final MessageEncoder messageEncoder;
+	private final MessageTracker messageTracker;
+	private final Clock clock;
+
+	ProtocolEngineImpl(DatabaseComponent db, ClientHelper clientHelper,
+			MessageEncoder messageEncoder, MessageParser<S> messageParser,
+			MessageTracker messageTracker, Clock clock) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+		this.messageEncoder = messageEncoder;
+		this.messageParser = messageParser;
+		this.messageTracker = messageTracker;
+		this.clock = clock;
+	}
+
+	@Override
+	public Session onInviteAction(Transaction txn, Session s,
+			@Nullable String message, long timestamp) throws DbException {
+		switch (s.getState()) {
+			case START:
+			case REMOTE_LEFT:
+				return onLocalInvite(txn, s, message, timestamp);
+			case LOCAL_INVITED:
+			case REMOTE_INVITED:
+			case SHARING:
+			case LOCAL_LEFT:
+			case REMOTE_HANGING:
+			case ERROR:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onLocalInvite(Transaction txn, Session s,
+			@Nullable String message, long timestamp) throws DbException {
+		// Send an INVITE message
+		Message sent = sendInviteMessage(txn, s, message, timestamp);
+		// Track the message
+		messageTracker.trackOutgoingMessage(txn, sent);
+		// Make the shareable visible to the contact
+		try {
+			setShareableVisibility(txn, s, VISIBLE);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
+		// Move to the REMOTE_INVITED state
+		return new Session(REMOTE_INVITED, s.getContactGroupId(),
+				s.getShareableId(), sent.getId(), s.getLastRemoteMessageId(),
+				sent.getTimestamp(), s.getInviteTimestamp());
+	}
+
+	private Message sendInviteMessage(Transaction txn, Session s,
+			@Nullable String message, long timestamp) throws DbException {
+		Group g = db.getGroup(txn, s.getShareableId());
+		BdfList descriptor;
+		try {
+			descriptor = clientHelper.toList(g.getDescriptor());
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group descriptor
+		}
+		long localTimestamp = Math.max(timestamp, getLocalTimestamp(s));
+		Message m = messageEncoder
+				.encodeInviteMessage(s.getContactGroupId(), localTimestamp,
+						s.getLastLocalMessageId(), descriptor, message);
+		sendMessage(txn, m, INVITE, s.getShareableId(), true);
+		return m;
+	}
+
+	@Override
+	public Session onAcceptAction(Transaction txn, Session s)
+			throws DbException {
+		switch (s.getState()) {
+			case LOCAL_INVITED:
+				return onLocalAccept(txn, s);
+			case START:
+			case REMOTE_INVITED:
+			case SHARING:
+			case LOCAL_LEFT:
+			case REMOTE_LEFT:
+			case REMOTE_HANGING:
+			case ERROR:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onLocalAccept(Transaction txn, Session s)
+			throws DbException {
+		// Mark the invite message unavailable to answer
+		MessageId inviteId = s.getLastRemoteMessageId();
+		if (inviteId == null) throw new IllegalStateException();
+		markMessageAvailableToAnswer(txn, inviteId, false);
+		// Send a ACCEPT message
+		Message sent = sendAcceptMessage(txn, s);
+		// Track the message
+		messageTracker.trackOutgoingMessage(txn, sent);
+		try {
+			// Add and subscribe to the shareable
+			addShareable(txn, inviteId);
+			// Share the shareable with the contact
+			setShareableVisibility(txn, s, SHARED);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
+		// Move to the SHARING state
+		return new Session(SHARING, s.getContactGroupId(), s.getShareableId(),
+				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	protected abstract void addShareable(Transaction txn, MessageId inviteId)
+			throws DbException, FormatException;
+
+	private Message sendAcceptMessage(Transaction txn, Session session)
+			throws DbException {
+		Message m = messageEncoder.encodeAcceptMessage(
+				session.getContactGroupId(), session.getShareableId(),
+				getLocalTimestamp(session), session.getLastLocalMessageId());
+		sendMessage(txn, m, ACCEPT, session.getShareableId(), true);
+		return m;
+	}
+
+	@Override
+	public Session onDeclineAction(Transaction txn, Session s)
+			throws DbException {
+		switch (s.getState()) {
+			case LOCAL_INVITED:
+				return onLocalDecline(txn, s);
+			case START:
+			case REMOTE_INVITED:
+			case SHARING:
+			case LOCAL_LEFT:
+			case REMOTE_LEFT:
+			case REMOTE_HANGING:
+			case ERROR:
+				throw new ProtocolStateException(); // Invalid in these states
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onLocalDecline(Transaction txn, Session s)
+			throws DbException {
+		// Mark the invite message unavailable to answer
+		MessageId inviteId = s.getLastRemoteMessageId();
+		if (inviteId == null) throw new IllegalStateException();
+		markMessageAvailableToAnswer(txn, inviteId, false);
+		// Send a DECLINE message
+		Message sent = sendDeclineMessage(txn, s);
+		// Track the message
+		messageTracker.trackOutgoingMessage(txn, sent);
+		// Move to the START state
+		return new Session(START, s.getContactGroupId(), s.getShareableId(),
+				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	private Message sendDeclineMessage(Transaction txn, Session session)
+			throws DbException {
+		Message m = messageEncoder.encodeDeclineMessage(
+				session.getContactGroupId(), session.getShareableId(),
+				getLocalTimestamp(session), session.getLastLocalMessageId());
+		sendMessage(txn, m, DECLINE, session.getShareableId(), true);
+		return m;
+	}
+
+	@Override
+	public Session onLeaveAction(Transaction txn, Session s)
+			throws DbException {
+		switch (s.getState()) {
+			case REMOTE_INVITED:
+				return onLocalLeave(txn, s, REMOTE_HANGING);
+			case SHARING:
+				return onLocalLeave(txn, s, LOCAL_LEFT);
+			case REMOTE_LEFT:
+				return onLocalLeave(txn, s, START);
+			case START:
+			case LOCAL_INVITED:
+			case LOCAL_LEFT:
+			case REMOTE_HANGING:
+			case ERROR:
+				return s; // Ignored in this state
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onLocalLeave(Transaction txn, Session s, State nextState)
+			throws DbException {
+		try {
+			// Stop sharing the shareable (not actually needed in REMOTE_LEFT)
+			setShareableVisibility(txn, s, INVISIBLE);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
+		// Send a LEAVE message
+		Message sent = sendLeaveMessage(txn, s);
+		// Move to the next state
+		return new Session(nextState, s.getContactGroupId(), s.getShareableId(),
+				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	private Message sendLeaveMessage(Transaction txn, Session session)
+			throws DbException {
+		Message m = messageEncoder.encodeLeaveMessage(
+				session.getContactGroupId(), session.getShareableId(),
+				getLocalTimestamp(session), session.getLastLocalMessageId());
+		sendMessage(txn, m, LEAVE, session.getShareableId(), false);
+		return m;
+	}
+
+	@Override
+	public Session onInviteMessage(Transaction txn, Session s,
+			InviteMessage<S> m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case START:
+			case LOCAL_LEFT:
+				return onRemoteInvite(txn, s, m, true, LOCAL_INVITED);
+			case REMOTE_INVITED:
+				return onRemoteInviteWhenInvited(txn, s, m);
+			case REMOTE_HANGING:
+				return onRemoteInvite(txn, s, m, false, LOCAL_LEFT);
+			case LOCAL_INVITED:
+			case SHARING:
+			case REMOTE_LEFT:
+				return abort(txn, s); // Invalid in these states
+			case ERROR:
+				return s; // Ignored in this state
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onRemoteInvite(Transaction txn, Session s,
+			InviteMessage<S> m, boolean available, State nextState)
+			throws DbException, FormatException {
+		// The timestamp must be higher than the last invite message, if any
+		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Mark the invite message visible in the UI and (un)available to answer
+		markMessageVisibleInUi(txn, m.getId(), true);
+		markMessageAvailableToAnswer(txn, m.getId(), available);
+		// Track the message
+		messageTracker.trackMessage(txn, m.getContactGroupId(),
+				m.getTimestamp(), false);
+		// Broadcast an event
+		ContactId contactId = getContactId(txn, s.getContactGroupId());
+		txn.attach(getInvitationRequestReceivedEvent(m, contactId, available,
+				false));
+		// Move to the next state
+		return new Session(nextState, s.getContactGroupId(), s.getShareableId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				m.getTimestamp());
+	}
+
+	private Session onRemoteInviteWhenInvited(Transaction txn, Session s,
+			InviteMessage<S> m) throws DbException, FormatException {
+		// The timestamp must be higher than the last invite message, if any
+		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Mark the invite message visible in the UI and unavailable to answer
+		markMessageVisibleInUi(txn, m.getId(), true);
+		markMessageAvailableToAnswer(txn, m.getId(), false);
+		// Track the message
+		messageTracker.trackMessage(txn, m.getContactGroupId(),
+				m.getTimestamp(), false);
+		// Share the shareable with the contact
+		setShareableVisibility(txn, s, SHARED);
+		// Broadcast an event
+		ContactId contactId = getContactId(txn, s.getContactGroupId());
+		txn.attach(
+				getInvitationRequestReceivedEvent(m, contactId, false, true));
+		// Move to the next state
+		return new Session(SHARING, s.getContactGroupId(), s.getShareableId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				m.getTimestamp());
+	}
+
+	abstract Event getInvitationRequestReceivedEvent(InviteMessage<S> m,
+			ContactId contactId, boolean available, boolean canBeOpened);
+
+	@Override
+	public Session onAcceptMessage(Transaction txn, Session s,
+			AcceptMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case REMOTE_INVITED:
+				return onRemoteAcceptWhenInvited(txn, s, m);
+			case REMOTE_HANGING:
+				return onRemoteAccept(txn, s, m, LOCAL_LEFT);
+			case START:
+			case LOCAL_INVITED:
+			case SHARING:
+			case LOCAL_LEFT:
+			case REMOTE_LEFT:
+				return abort(txn, s); // Invalid in these states
+			case ERROR:
+				return s; // Ignored in this state
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onRemoteAccept(Transaction txn, Session s, AcceptMessage m,
+			State nextState) throws DbException, FormatException {
+		// The timestamp must be higher than the last invite message
+		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Mark the response visible in the UI
+		markMessageVisibleInUi(txn, m.getId(), true);
+		// Track the message
+		messageTracker.trackMessage(txn, m.getContactGroupId(),
+				m.getTimestamp(), false);
+		// Broadcast an event
+		ContactId contactId = getContactId(txn, m.getContactGroupId());
+		txn.attach(getInvitationResponseReceivedEvent(m, contactId));
+		// Move to the next state
+		return new Session(nextState, s.getContactGroupId(), s.getShareableId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	private Session onRemoteAcceptWhenInvited(Transaction txn, Session s,
+			AcceptMessage m) throws DbException, FormatException {
+		// Perform normal remote accept validation and operation
+		Session session = onRemoteAccept(txn, s, m, SHARING);
+		// Share the shareable with the contact
+		if (session.getState() != ERROR)
+			setShareableVisibility(txn, s, SHARED);
+		return session;
+	}
+
+	abstract Event getInvitationResponseReceivedEvent(AcceptMessage m,
+			ContactId contactId);
+
+	@Override
+	public Session onDeclineMessage(Transaction txn, Session s,
+			DeclineMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case REMOTE_INVITED:
+			case REMOTE_HANGING:
+				return onRemoteDecline(txn, s, m);
+			case START:
+			case LOCAL_INVITED:
+			case SHARING:
+			case LOCAL_LEFT:
+			case REMOTE_LEFT:
+				return abort(txn, s); // Invalid in these states
+			case ERROR:
+				return s; // Ignored in this state
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onRemoteDecline(Transaction txn, Session s,
+			DeclineMessage m) throws DbException, FormatException {
+		// The timestamp must be higher than the last invite message
+		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Mark the response visible in the UI
+		markMessageVisibleInUi(txn, m.getId(), true);
+		// Track the message
+		messageTracker.trackMessage(txn, m.getContactGroupId(),
+				m.getTimestamp(), false);
+		// Make the shareable invisible (not actually needed in REMOTE_HANGING)
+		try {
+			setShareableVisibility(txn, s, INVISIBLE);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
+		// Broadcast an event
+		ContactId contactId = getContactId(txn, m.getContactGroupId());
+		txn.attach(getInvitationResponseReceivedEvent(m, contactId));
+		// Move to the next state
+		return new Session(START, s.getContactGroupId(), s.getShareableId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	abstract Event getInvitationResponseReceivedEvent(DeclineMessage m,
+			ContactId contactId);
+
+	@Override
+	public Session onLeaveMessage(Transaction txn, Session s,
+			LeaveMessage m) throws DbException, FormatException {
+		switch (s.getState()) {
+			case LOCAL_INVITED:
+				return onRemoteLeaveWhenInvited(txn, s, m, START);
+			case LOCAL_LEFT:
+				return onRemoteLeave(txn, s, m, START);
+			case SHARING:
+				return onRemoteLeaveWhenSharing(txn, s, m);
+			case START:
+			case REMOTE_INVITED:
+			case REMOTE_LEFT:
+			case REMOTE_HANGING:
+				return abort(txn, s); // Invalid in these states
+			case ERROR:
+				return s; // Ignored in this state
+			default:
+				throw new AssertionError();
+		}
+	}
+
+	private Session onRemoteLeaveWhenInvited(Transaction txn, Session s,
+			LeaveMessage m, State nextState)
+			throws DbException, FormatException {
+		// Carry out normal leave validation and operation
+		Session session = onRemoteLeave(txn, s, m, nextState);
+		// Mark any invite messages in the session unavailable to answer
+		if (session.getState() != ERROR)
+			markInvitesUnavailableToAnswer(txn, s);
+		// Move to the next state
+		return session;
+	}
+
+	private Session onRemoteLeave(Transaction txn, Session s,
+			LeaveMessage m, State nextState)
+			throws DbException, FormatException {
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Move to the next state
+		return new Session(nextState, s.getContactGroupId(), s.getShareableId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	private Session onRemoteLeaveWhenSharing(Transaction txn, Session s,
+			LeaveMessage m) throws DbException, FormatException {
+		// Carry out normal leave validation and operation
+		Session session = onRemoteLeave(txn, s, m, REMOTE_LEFT);
+		// Stop sharing the shareable with the contact
+		if (session.getState() != ERROR)
+			setShareableVisibility(txn, s, INVISIBLE);
+		// Move to the next state
+		return session;
+	}
+
+	@Override
+	public Session onAbortMessage(Transaction txn, Session s, AbortMessage m)
+			throws DbException, FormatException {
+		return abort(txn, s);
+	}
+
+	private Session abort(Transaction txn, Session s)
+			throws DbException, FormatException {
+		// If the session has already been aborted, do nothing
+		if (s.getState() == ERROR) return s;
+		// Mark any invite messages in the session unavailable to answer
+		markInvitesUnavailableToAnswer(txn, s);
+		// If we subscribe, make the shareable invisible to the contact
+		if (isSubscribed(txn, s.getShareableId()))
+			setShareableVisibility(txn, s, INVISIBLE);
+		// Send an ABORT message
+		Message sent = sendAbortMessage(txn, s);
+		// Move to the ERROR state
+		return new Session(ERROR, s.getContactGroupId(), s.getShareableId(),
+				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
+				s.getInviteTimestamp());
+	}
+
+	private void markInvitesUnavailableToAnswer(Transaction txn, Session s)
+			throws DbException, FormatException {
+		GroupId shareableId = s.getShareableId();
+		BdfDictionary query =
+				messageParser.getInvitesAvailableToAnswerQuery(shareableId);
+		Map<MessageId, BdfDictionary> results =
+				clientHelper.getMessageMetadataAsDictionary(txn,
+						s.getContactGroupId(), query);
+		for (MessageId m : results.keySet())
+			markMessageAvailableToAnswer(txn, m, false);
+	}
+
+	private boolean isSubscribed(Transaction txn, GroupId g)
+			throws DbException {
+		if (!db.containsGroup(txn, g)) return false;
+		Group group = db.getGroup(txn, g);
+		return group.getClientId().equals(getClientId());
+	}
+
+	protected abstract ClientId getClientId();
+
+	private Message sendAbortMessage(Transaction txn, Session session)
+			throws DbException {
+		Message m = messageEncoder.encodeAbortMessage(
+				session.getContactGroupId(), session.getShareableId(),
+				getLocalTimestamp(session), session.getLastLocalMessageId());
+		sendMessage(txn, m, ABORT, session.getShareableId(), false);
+		return m;
+	}
+
+	private void sendMessage(Transaction txn, Message m, MessageType type,
+			GroupId shareableId, boolean visibleInConversation)
+			throws DbException {
+		BdfDictionary meta = messageEncoder.encodeMetadata(type, shareableId,
+				m.getTimestamp(), true, true, visibleInConversation, false);
+		try {
+			clientHelper.addLocalMessage(txn, m, meta, true);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	private void markMessageAvailableToAnswer(Transaction txn, MessageId m,
+			boolean available) throws DbException {
+		BdfDictionary meta = new BdfDictionary();
+		messageEncoder.setAvailableToAnswer(meta, available);
+		try {
+			clientHelper.mergeMessageMetadata(txn, m, meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	private void markMessageVisibleInUi(Transaction txn, MessageId m,
+			boolean visible) throws DbException {
+		BdfDictionary meta = new BdfDictionary();
+		messageEncoder.setVisibleInUi(meta, visible);
+		try {
+			clientHelper.mergeMessageMetadata(txn, m, meta);
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	private void setShareableVisibility(Transaction txn, Session session,
+			Visibility v) throws DbException, FormatException {
+		ContactId contactId = getContactId(txn, session.getContactGroupId());
+		db.setGroupVisibility(txn, contactId, session.getShareableId(), v);
+	}
+
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
+				contactGroupId);
+		return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
+	}
+
+	private boolean isValidDependency(Session session,
+			@Nullable MessageId dependency) {
+		MessageId expected = session.getLastRemoteMessageId();
+		if (dependency == null) return expected == null;
+		return expected != null && dependency.equals(expected);
+	}
+
+	private long getLocalTimestamp(Session session) {
+		return Math.max(clock.currentTimeMillis(),
+				Math.max(session.getLocalTimestamp(),
+						session.getInviteTimestamp()) + 1);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java b/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e0a5520180d7b3ea984d8b098f324953e109ae0
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java
@@ -0,0 +1,69 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.sharing.State.START;
+
+@Immutable
+@NotNullByDefault
+class Session {
+
+	private final State state;
+	private final GroupId contactGroupId, shareableId;
+	@Nullable
+	private final MessageId lastLocalMessageId, lastRemoteMessageId;
+	private final long localTimestamp, inviteTimestamp;
+
+	Session(State state, GroupId contactGroupId, GroupId shareableId,
+			@Nullable MessageId lastLocalMessageId,
+			@Nullable MessageId lastRemoteMessageId, long localTimestamp,
+			long inviteTimestamp) {
+		this.state = state;
+		this.contactGroupId = contactGroupId;
+		this.shareableId = shareableId;
+		this.lastLocalMessageId = lastLocalMessageId;
+		this.lastRemoteMessageId = lastRemoteMessageId;
+		this.localTimestamp = localTimestamp;
+		this.inviteTimestamp = inviteTimestamp;
+	}
+
+	Session(GroupId contactGroupId, GroupId shareableId) {
+		this(START, contactGroupId, shareableId, null, null, 0, 0);
+	}
+
+	public State getState() {
+		return state;
+	}
+
+	GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	GroupId getShareableId() {
+		return shareableId;
+	}
+
+	@Nullable
+	MessageId getLastLocalMessageId() {
+		return lastLocalMessageId;
+	}
+
+	@Nullable
+	MessageId getLastRemoteMessageId() {
+		return lastRemoteMessageId;
+	}
+
+	long getLocalTimestamp() {
+		return localTimestamp;
+	}
+
+	long getInviteTimestamp() {
+		return inviteTimestamp;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..61862e8936612d168f425ad1992b7871f244d145
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java
@@ -0,0 +1,11 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface SessionEncoder {
+
+	BdfDictionary encodeSession(Session s);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..7cf2bf139d67495092dec01dc158674aec217a63
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java
@@ -0,0 +1,46 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.concurrent.Immutable;
+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_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;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SHAREABLE_ID;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_STATE;
+
+@Immutable
+@NotNullByDefault
+class SessionEncoderImpl implements SessionEncoder {
+
+	@Inject
+	SessionEncoderImpl() {
+	}
+
+	@Override
+	public BdfDictionary encodeSession(Session s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_SESSION_ID, s.getShareableId());
+		d.put(SESSION_KEY_SHAREABLE_ID, s.getShareableId());
+		MessageId lastLocalMessageId = s.getLastLocalMessageId();
+		if (lastLocalMessageId == null)
+			d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, NULL_VALUE);
+		else d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, lastLocalMessageId);
+		MessageId lastRemoteMessageId = s.getLastRemoteMessageId();
+		if (lastRemoteMessageId == null)
+			d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, NULL_VALUE);
+		else d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, lastRemoteMessageId);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp());
+		d.put(SESSION_KEY_INVITE_TIMESTAMP, s.getInviteTimestamp());
+		d.put(SESSION_KEY_STATE, s.getState().getValue());
+		return d;
+	}
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..a82aed3eca199a2eb8eb2924375595a88ba42a0e
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java
@@ -0,0 +1,17 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.api.client.SessionId;
+
+@NotNullByDefault
+interface SessionParser {
+
+	BdfDictionary getSessionQuery(SessionId s);
+
+	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
new file mode 100644
index 0000000000000000000000000000000000000000..62d9c44b12b276ea27655471b916451597ceedcb
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java
@@ -0,0 +1,75 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+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_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;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SHAREABLE_ID;
+import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_STATE;
+
+@Immutable
+@NotNullByDefault
+class SessionParserImpl implements SessionParser {
+
+	@Inject
+	SessionParserImpl() {
+	}
+
+	@Override
+	public BdfDictionary getSessionQuery(SessionId s) {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
+	}
+
+	@Override
+	public Session parseSession(GroupId contactGroupId,
+			BdfDictionary d) throws FormatException {
+		return new Session(State.fromValue(getState(d)), contactGroupId,
+				getShareableId(d), getLastLocalMessageId(d),
+				getLastRemoteMessageId(d), getLocalTimestamp(d),
+				getInviteTimestamp(d));
+	}
+
+	private int getState(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_STATE).intValue();
+	}
+
+	private GroupId getShareableId(BdfDictionary d) throws FormatException {
+		return new GroupId(d.getRaw(SESSION_KEY_SHAREABLE_ID));
+	}
+
+	@Nullable
+	private MessageId getLastLocalMessageId(BdfDictionary d)
+			throws FormatException {
+		byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		return b == null ? null : new MessageId(b);
+	}
+
+	@Nullable
+	private MessageId getLastRemoteMessageId(BdfDictionary d)
+			throws FormatException {
+		byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		return b == null ? null : new MessageId(b);
+	}
+
+	private long getLocalTimestamp(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+	}
+
+	private long getInviteTimestamp(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_INVITE_TIMESTAMP);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java
index 2d18d56ceda10d736f8bfab22d6c3af2799521cb..367f8882abaa94418b053bbf7113508bd2cccc8e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.briar.api.sharing.Shareable;
 import org.briarproject.briar.api.sharing.SharingMessage;
 
+@Deprecated
 @NotNullByDefault
 interface ShareableFactory<S extends Shareable, I extends SharingMessage.Invitation, IS extends InviteeSessionState, SS extends SharerSessionState> {
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java
index 7d5b351834557775d4f7a634400a5f6514938813..eb05d87cdffc6b91aaa773bb7f5fc463a9d24f90 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java
@@ -29,6 +29,7 @@ import static org.briarproject.briar.api.sharing.SharingMessage.SimpleMessage;
 import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_ACCEPT;
 import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_DECLINE;
 
+@Deprecated
 @Immutable
 @NotNullByDefault
 class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent>
@@ -37,12 +38,12 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 	private static final Logger LOG =
 			Logger.getLogger(SharerEngine.class.getName());
 
-	private final InvitationFactory<I, SS> invitationFactory;
+	private final OldInvitationFactory<I, SS> invitationFactory;
 	private final InvitationResponseReceivedEventFactory<SS, IRR>
 			invitationResponseReceivedEventFactory;
 	private final Clock clock;
 
-	SharerEngine(InvitationFactory<I, SS> invitationFactory,
+	SharerEngine(OldInvitationFactory<I, SS> invitationFactory,
 			InvitationResponseReceivedEventFactory<SS, IRR> invitationResponseReceivedEventFactory,
 			Clock clock) {
 		this.invitationFactory = invitationFactory;
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java
index 597c88c688d9f23e65ac3662317c14083bd150e5..2579a3f509023345c3168943e4deb91e6c754480 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java
@@ -23,6 +23,7 @@ import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_AC
 import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_DECLINE;
 import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_LEAVE;
 
+@Deprecated
 @NotThreadSafe
 @NotNullByDefault
 public abstract class SharerSessionState extends SharingSessionState {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java
index a0a36f8f8995d90bcb16d18225e439082dd269ce..9be12463f78edd0dc85c3e033da3d54322ae36bf 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.sharing.Shareable;
 
+@Deprecated
 @NotNullByDefault
 interface SharerSessionStateFactory<S extends Shareable, SS extends SharerSessionState> {
 
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
new file mode 100644
index 0000000000000000000000000000000000000000..d860abc8f2b993e61adb46664c51575e41a9a546
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
@@ -0,0 +1,28 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.briar.client.MessageTrackerConstants;
+
+interface SharingConstants {
+
+	// Group metadata keys
+	String GROUP_KEY_CONTACT_ID = "contactId";
+
+	// Message metadata keys
+	String MSG_KEY_MESSAGE_TYPE = "messageType";
+	String MSG_KEY_SHAREABLE_ID = "shareableId";
+	String MSG_KEY_TIMESTAMP = "timestamp";
+	String MSG_KEY_READ = MessageTrackerConstants.MSG_KEY_READ;
+	String MSG_KEY_LOCAL = "local";
+	String MSG_KEY_VISIBLE_IN_UI = "visibleInUi";
+	String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
+
+	// Session keys
+	String SESSION_KEY_STATE = "state";
+	String SESSION_KEY_SESSION_ID = "sessionId";
+	String SESSION_KEY_SHAREABLE_ID = "shareableId";
+	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
+	String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
+	String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
+	String SESSION_KEY_INVITE_TIMESTAMP = "inviteTimestamp";
+
+}
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 c6be693e5d8b5e000ad35e88b1e7984058da49f2..e0c640e1e6258c1a8d4a905b14a3b840f1069a04 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
@@ -1,6 +1,5 @@
 package org.briarproject.briar.sharing;
 
-import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
@@ -9,17 +8,12 @@ import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.contact.ContactManager.AddContactHook;
 import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook;
 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.data.MetadataParser;
 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.NoSuchMessageException;
 import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.event.Event;
-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.ClientId;
@@ -28,127 +22,65 @@ 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.system.Clock;
-import org.briarproject.bramble.util.StringUtils;
-import org.briarproject.briar.api.client.MessageQueueManager;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.sharing.InvitationMessage;
+import org.briarproject.briar.api.sharing.InvitationRequest;
+import org.briarproject.briar.api.sharing.InvitationResponse;
 import org.briarproject.briar.api.sharing.Shareable;
 import org.briarproject.briar.api.sharing.SharingInvitationItem;
 import org.briarproject.briar.api.sharing.SharingManager;
-import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
-import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
-import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
 import org.briarproject.briar.client.ConversationClientImpl;
 
-import java.io.IOException;
-import java.security.SecureRandom;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
+import java.util.Map.Entry;
 
 import javax.annotation.Nullable;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
-import static org.briarproject.briar.api.client.ProtocolEngine.StateUpdate;
-import static org.briarproject.briar.api.sharing.SharingConstants.CONTACT_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.IS_SHARER;
-import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
-import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_BY_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_WITH_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARING_SALT_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.STATE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHARED_SHAREABLE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_SHARE_SHAREABLE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_BY_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_WITH_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
-import static org.briarproject.briar.api.sharing.SharingConstants.TO_BE_SHARED_BY_US;
-import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
-import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage;
-import static org.briarproject.briar.api.sharing.SharingMessage.Invitation;
-import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
-import static org.briarproject.briar.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE;
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
+import static org.briarproject.briar.sharing.SharingConstants.GROUP_KEY_CONTACT_ID;
 
 @NotNullByDefault
-abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent>
+abstract class SharingManagerImpl<S extends Shareable>
 		extends ConversationClientImpl
 		implements SharingManager<S>, Client, AddContactHook,
 		RemoveContactHook {
 
-	private static final Logger LOG =
-			Logger.getLogger(SharingManagerImpl.class.getName());
-
-	private final MessageQueueManager messageQueueManager;
-	private final MetadataEncoder metadataEncoder;
-	private final SecureRandom random;
+	private final MessageParser<S> messageParser;
+	private final SessionEncoder sessionEncoder;
+	private final SessionParser sessionParser;
 	private final ContactGroupFactory contactGroupFactory;
-	private final Clock clock;
-	private final Group localGroup;
-
-	SharingManagerImpl(DatabaseComponent db,
-			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
-			MetadataParser metadataParser, MetadataEncoder metadataEncoder,
-			SecureRandom random, ContactGroupFactory contactGroupFactory,
-			MessageTracker messageTracker, Clock clock) {
+	private final ProtocolEngine<S> engine;
+	private final InvitationFactory<S> invitationFactory;
+
+	SharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			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.messageQueueManager = messageQueueManager;
-		this.metadataEncoder = metadataEncoder;
-		this.random = random;
+		this.messageParser = messageParser;
+		this.sessionEncoder = sessionEncoder;
+		this.sessionParser = sessionParser;
 		this.contactGroupFactory = contactGroupFactory;
-		this.clock = clock;
-		localGroup = contactGroupFactory.createLocalGroup(getClientId());
+		this.engine = engine;
+		this.invitationFactory = invitationFactory;
 	}
 
 	protected abstract ClientId getClientId();
 
-	protected abstract InvitationMessage createInvitationRequest(MessageId id,
-			I msg, ContactId contactId, GroupId shareableId, boolean available,
-			boolean canBeOpened, long time, boolean local, boolean sent,
-			boolean seen, boolean read);
-
-	protected abstract InvitationMessage createInvitationResponse(MessageId id,
-			SessionId sessionId, GroupId groupId, ContactId contactId,
-			GroupId shareableId, boolean accept, long time, boolean local,
-			boolean sent, boolean seen, boolean read);
-
-	protected abstract ShareableFactory<S, I, IS, SS> getSFactory();
-
-	protected abstract InvitationFactory<I, SS> getIFactory();
-
-	protected abstract InviteeSessionStateFactory<S, IS> getISFactory();
-
-	protected abstract SharerSessionStateFactory<S, SS> getSSFactory();
-
-	protected abstract InvitationReceivedEventFactory<IS, IR> getIRFactory();
-
-	protected abstract InvitationResponseReceivedEventFactory<SS, IRR> getIRRFactory();
-
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
-		db.addGroup(txn, localGroup);
 		// Ensure we've set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
@@ -165,10 +97,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 			// Attach the contact ID to the group
 			BdfDictionary meta = new BdfDictionary();
-			meta.put(CONTACT_ID, c.getId().getInt());
-			meta.put(TO_BE_SHARED_BY_US, new BdfList());
-			meta.put(SHARED_BY_US, new BdfList());
-			meta.put(SHARED_WITH_US, new BdfList());
+			meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
 			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -177,152 +106,132 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
-		// query for this contact c
-		BdfDictionary query = BdfDictionary.of(
-				new BdfEntry(CONTACT_ID, c.getId().getInt())
-		);
-
-		// clean up session states with that contact from localGroup
-		try {
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
-							query);
-			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
-				deleteMessage(txn, entry.getKey());
-			}
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-
 		// 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(), c);
+	}
+
 	@Override
 	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
 			BdfDictionary d) throws DbException, FormatException {
-
-		BaseMessage msg = BaseMessage.from(getIFactory(), m.getGroupId(), d);
-		SessionId sessionId = msg.getSessionId();
-
-		if (msg.getType() == SHARE_MSG_TYPE_INVITATION) {
-			// we are an invitee who just received a new invitation
-			boolean stateExists = true;
-			try {
-				// check if we have a session with that ID already
-				getSessionState(txn, sessionId, false);
-			} catch (FormatException e) {
-				// this is what we would expect under normal circumstances
-				stateExists = false;
-			}
-			// check if we already have a state with that sessionId
-			if (stateExists) throw new FormatException();
-
-			// check if shareable can be shared
-			I invitation = (I) msg;
-			S f = getSFactory().parse(invitation);
-			ContactId contactId = getContactId(txn, m.getGroupId());
-			Contact contact = db.getContact(txn, contactId);
-			if (!canBeShared(txn, f.getId(), contact))
-				checkForRaceCondition(txn, f, contact);
-
-			// initialize state and process invitation
-			IS state = initializeInviteeState(txn, contactId, invitation,
-					m.getId());
-			InviteeEngine<IS, IR> engine =
-					new InviteeEngine<IS, IR>(getIRFactory(), clock);
-			processInviteeStateUpdate(txn, m.getId(),
-					engine.onMessageReceived(state, msg));
-			messageTracker.trackIncomingMessage(txn, m);
-		} else if (msg.getType() == SHARE_MSG_TYPE_ACCEPT ||
-				msg.getType() == SHARE_MSG_TYPE_DECLINE) {
-			// we are a sharer who just received a response
-			SS state = getSessionStateForSharer(txn, sessionId);
-			state.setResponseId(m.getId());
-			SharerEngine<I, SS, IRR> engine =
-					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory(), clock);
-			processSharerStateUpdate(txn, m.getId(),
-					engine.onMessageReceived(state, msg));
-			messageTracker.trackIncomingMessage(txn, m);
-		} else if (msg.getType() == SHARE_MSG_TYPE_LEAVE ||
-				msg.getType() == SHARE_MSG_TYPE_ABORT) {
-			// we don't know who we are, so figure it out
-			SharingSessionState s = getSessionState(txn, sessionId, true);
-			if (s instanceof SharerSessionState) {
-				// we are a sharer and the invitee wants to leave or abort
-				SS state = (SS) s;
-				SharerEngine<I, SS, IRR> engine =
-						new SharerEngine<I, SS, IRR>(getIFactory(),
-								getIRRFactory(), clock);
-				processSharerStateUpdate(txn, m.getId(),
-						engine.onMessageReceived(state, msg));
-			} else {
-				// we are an invitee and the sharer wants to leave or abort
-				IS state = (IS) s;
-				InviteeEngine<IS, IR> engine =
-						new InviteeEngine<IS, IR>(getIRFactory(), clock);
-				processInviteeStateUpdate(txn, m.getId(),
-						engine.onMessageReceived(state, msg));
-			}
+		// Parse the metadata
+		MessageMetadata meta = messageParser.parseMetadata(d);
+		// Look up the session, if there is one
+		SessionId sessionId = getSessionId(meta.getShareableId());
+		StoredSession ss = getSession(txn, m.getGroupId(), sessionId);
+		// Handle the message
+		Session session;
+		MessageId storageId;
+		if (ss == null) {
+			session = handleFirstMessage(txn, m, body, meta);
+			storageId = createStorageId(txn, m.getGroupId());
 		} else {
-			// message has passed validator, so that should never happen
-			throw new AssertionError("Illegal Sharing Message");
+			session = handleMessage(txn, m, body, meta, ss.bdfSession);
+			storageId = ss.storageId;
 		}
-		// don't share message as other party already has it
+		// Store the updated session
+		storeSession(txn, storageId, session);
 		return false;
 	}
 
-	@Override
-	public void sendInvitation(GroupId groupId, ContactId contactId,
-			@Nullable String msg) throws DbException {
-
-		Transaction txn = db.startTransaction(false);
-		try {
-			// initialize local state for sharer
-			S f = getSFactory().get(txn, groupId);
-			SS localState = initializeSharerState(txn, f, contactId);
-
-			// add invitation message to local state to be available for engine
-			if (!StringUtils.isNullOrEmpty(msg)) {
-				int msgLength = StringUtils.toUtf8(msg).length;
-				if (msgLength > MAX_INVITATION_MESSAGE_LENGTH)
-					throw new IllegalArgumentException();
-				localState.setMessage(msg);
-			}
+	private SessionId getSessionId(GroupId shareableId) {
+		return new SessionId(shareableId.getBytes());
+	}
 
-			// start engine and process its state update
-			SharerEngine<I, SS, IRR> engine =
-					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory(), clock);
-			StateUpdate<SS, BaseMessage> update =
-					engine.onLocalAction(localState,
-							SharerSessionState.Action.LOCAL_INVITATION);
-			processSharerStateUpdate(txn, null, update);
+	@Nullable
+	private StoredSession getSession(Transaction txn, GroupId contactGroupId,
+			SessionId sessionId) throws DbException, FormatException {
+		BdfDictionary query = sessionParser.getSessionQuery(sessionId);
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+		if (results.size() > 1) throw new DbException();
+		if (results.isEmpty()) return null;
+		return new StoredSession(results.keySet().iterator().next(),
+				results.values().iterator().next());
+	}
 
-			// track message
-			// TODO handle this properly without engine hacks (#376)
-			long time = update.toSend.get(0).getTime();
-			messageTracker.trackMessage(txn, localState.getContactGroupId(), time,
-					true);
+	private Session handleFirstMessage(Transaction txn, Message m, BdfList body,
+			MessageMetadata meta) throws DbException, FormatException {
+		GroupId shareableId = meta.getShareableId();
+		MessageType type = meta.getMessageType();
+		if (type == INVITE) {
+			Session session = new Session(m.getGroupId(), shareableId);
+			BdfDictionary d = sessionEncoder.encodeSession(session);
+			return handleMessage(txn, m, body, meta, d);
+		} else {
+			throw new FormatException(); // Invalid first message
+		}
+	}
 
-			db.commitTransaction(txn);
-		} catch (FormatException e) {
-			throw new DbException();
-		} finally {
-			db.endTransaction(txn);
+	private Session handleMessage(Transaction txn, Message m, BdfList body,
+			MessageMetadata meta, BdfDictionary d)
+			throws DbException, FormatException {
+		MessageType type = meta.getMessageType();
+		Session session = sessionParser.parseSession(m.getGroupId(), d);
+		if (type == INVITE) {
+			InviteMessage<S> invite = messageParser.parseInviteMessage(m, body);
+			return engine.onInviteMessage(txn, session, invite);
+		} else if (type == ACCEPT) {
+			AcceptMessage accept = messageParser.parseAcceptMessage(m, body);
+			return engine.onAcceptMessage(txn, session, accept);
+		} else if (type == DECLINE) {
+			DeclineMessage decline = messageParser.parseDeclineMessage(m, body);
+			return engine.onDeclineMessage(txn, session, decline);
+		} else if (type == LEAVE) {
+			LeaveMessage leave = messageParser.parseLeaveMessage(m, body);
+			return engine.onLeaveMessage(txn, session, leave);
+		} else if (type == ABORT) {
+			AbortMessage abort = messageParser.parseAbortMessage(m, body);
+			return engine.onAbortMessage(txn, session, abort);
+		} else {
+			throw new AssertionError();
 		}
 	}
 
-	@Override
-	public void respondToInvitation(S f, Contact c, boolean accept)
+	private MessageId createStorageId(Transaction txn, GroupId g)
 			throws DbException {
+		Message m = clientHelper.createMessageForStoringMetadata(g);
+		db.addLocalMessage(txn, m, new Metadata(), false);
+		return m.getId();
+	}
+
+	private void storeSession(Transaction txn, MessageId storageId,
+			Session session) throws DbException, FormatException {
+		BdfDictionary d = sessionEncoder.encodeSession(session);
+		clientHelper.mergeMessageMetadata(txn, storageId, d);
+	}
 
+	@Override
+	public void sendInvitation(GroupId shareableId, ContactId contactId,
+			@Nullable String message, long timestamp) throws DbException {
+		SessionId sessionId = getSessionId(shareableId);
 		Transaction txn = db.startTransaction(false);
 		try {
-			// find session state based on shareable
-			IS localState = getSessionStateForResponse(txn, f, c);
-			respondToInvitation(txn, localState, accept);
+			// Look up the session, if there is one
+			Contact contact = db.getContact(txn, contactId);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			StoredSession ss = getSession(txn, contactGroupId, sessionId);
+			// Create or parse the session
+			Session session;
+			MessageId storageId;
+			if (ss == null) {
+				// This is the first invite - create a new session
+				session = new Session(contactGroupId, shareableId);
+				storageId = createStorageId(txn, contactGroupId);
+			} else {
+				// We already have a session
+				session = sessionParser
+						.parseSession(contactGroupId, ss.bdfSession);
+				storageId = ss.storageId;
+			}
+			// Handle the invite action
+			session = engine.onInviteAction(txn, session, message, timestamp);
+			// Store the updated session
+			storeSession(txn, storageId, session);
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -332,13 +241,29 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 	}
 
 	@Override
-	public void respondToInvitation(SessionId id, boolean accept)
+	public void respondToInvitation(S s, Contact c, boolean accept)
 			throws DbException {
+		respondToInvitation(c.getId(), getSessionId(s.getId()), accept);
+	}
 
+	@Override
+	public void respondToInvitation(ContactId c, SessionId id, boolean accept)
+			throws DbException {
 		Transaction txn = db.startTransaction(false);
 		try {
-			IS localState = (IS) getSessionState(txn, id, true);
-			respondToInvitation(txn, localState, accept);
+			// Look up the session
+			Contact contact = db.getContact(txn, c);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			StoredSession ss = getSession(txn, contactGroupId, id);
+			if (ss == null) throw new IllegalArgumentException();
+			// Parse the session
+			Session session =
+					sessionParser.parseSession(contactGroupId, ss.bdfSession);
+			// Handle the accept or decline action
+			if (accept) session = engine.onAcceptAction(txn, session);
+			else session = engine.onDeclineAction(txn, session);
+			// Store the updated session
+			storeSession(txn, ss.storageId, session);
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -347,144 +272,108 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
-	private void respondToInvitation(Transaction txn, IS localState,
-			boolean accept) throws DbException, FormatException {
-		// define action
-		InviteeSessionState.Action localAction;
-		if (accept) {
-			localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
-		} else {
-			localAction = InviteeSessionState.Action.LOCAL_DECLINE;
-		}
-
-		// start engine and process its state update
-		InviteeEngine<IS, IR> engine =
-				new InviteeEngine<IS, IR>(getIRFactory(), clock);
-		StateUpdate<IS, BaseMessage> update =
-				engine.onLocalAction(localState, localAction);
-		processInviteeStateUpdate(txn, null, update);
-
-		// track message
-		// TODO handle this properly without engine hacks (#376)
-		long time = update.toSend.get(0).getTime();
-		messageTracker.trackMessage(txn, localState.getContactGroupId(), time, true);
-	}
-
 	@Override
-	public Collection<InvitationMessage> getInvitationMessages(
-			ContactId contactId)
+	public Collection<InvitationMessage> getInvitationMessages(ContactId c)
 			throws DbException {
-
+		List<InvitationMessage> messages;
 		Transaction txn = db.startTransaction(true);
 		try {
-			Contact contact = db.getContact(txn, contactId);
-			Group group = getContactGroup(contact);
-
-			Collection<InvitationMessage> list =
-					new ArrayList<InvitationMessage>();
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn, group.getId());
-			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
-				BdfDictionary d = m.getValue();
-				long type = d.getLong(TYPE);
-				if (type == SHARE_MSG_TYPE_LEAVE ||
-						type == SHARE_MSG_TYPE_ABORT) continue;
-				try {
-					MessageStatus status =
-							db.getMessageStatus(txn, contactId, m.getKey());
-					SharingSessionState s;
-					long time = d.getLong(TIME);
-					boolean local = d.getBoolean(LOCAL);
-					boolean read = d.getBoolean(MSG_KEY_READ, false);
-					boolean available = false, canBeOpened = false;
-
-					if (type == SHARE_MSG_TYPE_INVITATION) {
-						I msg = getIFactory().build(group.getId(), d);
-						SessionId sessionId = msg.getSessionId();
-						s = getSessionState(txn, sessionId, true);
-						if (!local) {
-							// figure out whether the shareable is still available
-							if (!(s instanceof InviteeSessionState))
-								continue;
-							available = ((InviteeSessionState) s).getState() ==
-									AWAIT_LOCAL_RESPONSE;
-							if (!available) {
-								canBeOpened = db.containsGroup(txn,
-										s.getShareableId());
-							}
-						}
-						InvitationMessage im =
-								createInvitationRequest(m.getKey(), msg,
-										contactId, s.getShareableId(),
-										available, canBeOpened, time, local,
-										status.isSent(), status.isSeen(), read);
-						list.add(im);
-					} else if (type == SHARE_MSG_TYPE_ACCEPT ||
-							type == SHARE_MSG_TYPE_DECLINE) {
-						boolean accept = type == SHARE_MSG_TYPE_ACCEPT;
-						BaseMessage msg = BaseMessage
-								.from(getIFactory(), group.getId(), d);
-						SessionId sessionId = msg.getSessionId();
-						s = getSessionState(txn, sessionId, true);
-						InvitationMessage im =
-								createInvitationResponse(m.getKey(), sessionId,
-										group.getId(), contactId,
-										s.getShareableId(), accept, time, local,
-										status.isSent(), status.isSeen(), read);
-						list.add(im);
-					} else {
-						throw new RuntimeException("Unexpected Message Type");
-					}
-				} catch (FormatException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
+			Contact contact = db.getContact(txn, c);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
+			Map<MessageId, BdfDictionary> results = clientHelper
+					.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+			messages = new ArrayList<InvitationMessage>(results.size());
+			for (Entry<MessageId, BdfDictionary> e : results.entrySet()) {
+				MessageId m = e.getKey();
+				MessageMetadata meta =
+						messageParser.parseMetadata(e.getValue());
+				MessageStatus status = db.getMessageStatus(txn, c, m);
+				MessageType type = meta.getMessageType();
+				if (type == INVITE) {
+					messages.add(
+							parseInvitationRequest(txn, c, m, meta, status));
+				} else if (type == ACCEPT) {
+					messages.add(
+							parseInvitationResponse(c, contactGroupId, m, meta,
+									status, true));
+				} else if (type == DECLINE) {
+					messages.add(
+							parseInvitationResponse(c, contactGroupId, m, meta,
+									status, false));
 				}
 			}
 			db.commitTransaction(txn);
-			return list;
 		} catch (FormatException e) {
 			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
+		return messages;
+	}
+
+	private InvitationRequest parseInvitationRequest(Transaction txn,
+			ContactId c, MessageId m, MessageMetadata meta,
+			MessageStatus status) throws DbException, FormatException {
+		// Look up the invite message to get the details of the private group
+		InviteMessage<S> invite = messageParser.getInviteMessage(txn, m);
+		boolean canBeOpened = db.containsGroup(txn, invite.getShareableId());
+		return invitationFactory
+				.createInvitationRequest(meta.isLocal(), status.isSent(),
+						status.isSeen(), meta.isRead(), invite, c,
+						meta.isAvailableToAnswer(), canBeOpened);
+	}
+
+	private InvitationResponse parseInvitationResponse(ContactId c,
+			GroupId contactGroupId, MessageId m, MessageMetadata meta,
+			MessageStatus status, boolean accept)
+			throws DbException, FormatException {
+		return invitationFactory.createInvitationResponse(m, contactGroupId,
+				meta.getTimestamp(), meta.isLocal(), status.isSent(),
+				status.isSeen(), meta.isRead(), meta.getShareableId(), c,
+				accept);
 	}
 
 	@Override
 	public Collection<SharingInvitationItem> getInvitations()
 			throws DbException {
-		List<SharingInvitationItem> invitations =
+		List<SharingInvitationItem> items =
 				new ArrayList<SharingInvitationItem>();
+		BdfDictionary query = messageParser.getInvitesAvailableToAnswerQuery();
+		Map<S, Collection<Contact>> sharers =
+				new HashMap<S, Collection<Contact>>();
 		Transaction txn = db.startTransaction(true);
 		try {
-			Set<S> shareables = new HashSet<S>();
-			Map<GroupId, Collection<Contact>> newSharers =
-					new HashMap<GroupId, Collection<Contact>>();
-			Collection<Contact> contacts = db.getContacts(txn);
-
 			// get invitations from each contact
-			for (Contact contact : contacts) {
-				Collection<S> newShareables = getInvited(txn, contact);
-				shareables.addAll(newShareables);
-				for (S s : newShareables) {
-					if (newSharers.containsKey(s.getId())) {
-						newSharers.get(s.getId()).add(contact);
+			for (Contact c : db.getContacts(txn)) {
+				GroupId contactGroupId = getContactGroup(c).getId();
+				Map<MessageId, BdfDictionary> results =
+						clientHelper.getMessageMetadataAsDictionary(txn,
+								contactGroupId, query);
+				for (MessageId m : results.keySet()) {
+					InviteMessage<S> invite =
+							messageParser.getInviteMessage(txn, m);
+					S s = invite.getShareable();
+					if (sharers.containsKey(s)) {
+						sharers.get(s).add(c);
 					} else {
-						Collection<Contact> c = new ArrayList<Contact>();
-						c.add(contact);
-						newSharers.put(s.getId(), c);
+						Collection<Contact> contacts = new ArrayList<Contact>();
+						contacts.add(c);
+						sharers.put(s, contacts);
 					}
 				}
 			}
-			// construct InvitationItem objects
-			for (S s : shareables) {
-				Collection<Contact> newS = newSharers.get(s.getId());
+			// construct the invitation items
+			for (Entry<S, Collection<Contact>> e : sharers.entrySet()) {
+				S s = e.getKey();
+				Collection<Contact> contacts = e.getValue();
 				boolean subscribed = db.containsGroup(txn, s.getId());
 				SharingInvitationItem invitation =
-						new SharingInvitationItem(s, subscribed, newS);
-				invitations.add(invitation);
+						new SharingInvitationItem(s, subscribed, contacts);
+				items.add(invitation);
 			}
 			db.commitTransaction(txn);
-			return invitations;
+			return items;
 		} catch (FormatException e) {
 			throw new DbException(e);
 		} finally {
@@ -492,560 +381,75 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
-	private Collection<S> getInvited(Transaction txn, Contact contact)
-			throws DbException, FormatException {
-
-		// query for all external invitations
-		BdfDictionary query = BdfDictionary.of(
-				new BdfEntry(TYPE, SHARE_MSG_TYPE_INVITATION),
-				new BdfEntry(LOCAL, false)
-		);
-		Group group = getContactGroup(contact);
-
-		Set<S> invited = new HashSet<S>();
-		Map<MessageId, BdfDictionary> map = clientHelper
-				.getMessageMetadataAsDictionary(txn, group.getId(), query);
-		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
-			BdfDictionary d = m.getValue();
-			try {
-				I msg = getIFactory().build(group.getId(), d);
-				IS iss = (IS) getSessionState(txn, msg.getSessionId(), true);
-				// get and add the shareable if the invitation is unanswered
-				if (iss.getState().equals(AWAIT_LOCAL_RESPONSE)) {
-					S s = getSFactory().parse(iss);
-					invited.add(s);
-				}
-			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, e.toString(), e);
-			}
-		}
-		return invited;
-	}
-
 	@Override
 	public Collection<Contact> getSharedWith(GroupId g) throws DbException {
+		// TODO report also pending invitations
+		Collection<Contact> contacts = new ArrayList<Contact>();
+		Transaction txn = db.startTransaction(true);
 		try {
-			List<Contact> shared = new ArrayList<Contact>();
-			Transaction txn = db.startTransaction(true);
-			try {
-				for (Contact c : db.getContacts(txn)) {
-					GroupId contactGroup = getContactGroup(c).getId();
-					if (listContains(txn, contactGroup, g, SHARED_BY_US))
-						shared.add(c);
-					else if (listContains(txn, contactGroup, g, SHARED_WITH_US))
-						shared.add(c);
-				}
-				db.commitTransaction(txn);
-			} finally {
-				db.endTransaction(txn);
+			for (Contact c : db.getContacts(txn)) {
+				if (db.getGroupVisibility(txn, c.getId(), g) == SHARED)
+					contacts.add(c);
 			}
-			return shared;
-		} catch (FormatException e) {
-			throw new DbException(e);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
 		}
+		return contacts;
 	}
 
 	@Override
 	public boolean canBeShared(GroupId g, Contact c) throws DbException {
-		boolean canBeShared;
+		GroupId contactGroupId = getContactGroup(c).getId();
+		SessionId sessionId = getSessionId(g);
 		Transaction txn = db.startTransaction(true);
 		try {
-			canBeShared = canBeShared(txn, g, c);
+			StoredSession ss = getSession(txn, contactGroupId, sessionId);
 			db.commitTransaction(txn);
+			// If there's no session, we can share the group with the contact
+			if (ss == null) return true;
+			// If the session's in the right state, the contact can be invited
+			Session session =
+					sessionParser.parseSession(contactGroupId, ss.bdfSession);
+			return session.getState().canInvite();
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
-		return canBeShared;
 	}
 
-	protected boolean canBeShared(Transaction txn, GroupId g, Contact c)
+	protected void removingShareable(Transaction txn, S shareable)
 			throws DbException {
-
-		try {
-			GroupId contactGroup = getContactGroup(c).getId();
-			return !listContains(txn, contactGroup, g, SHARED_BY_US) &&
-					!listContains(txn, contactGroup, g, SHARED_WITH_US) &&
-					!listContains(txn, contactGroup, g, TO_BE_SHARED_BY_US);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		}
-	}
-
-	void removingShareable(Transaction txn, S f) throws DbException {
+		SessionId sessionId = getSessionId(shareable.getId());
+		// If we have any sessions in progress, tell the contacts we're leaving
 		try {
 			for (Contact c : db.getContacts(txn)) {
-				GroupId g = getContactGroup(c).getId();
-				if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) {
-					leaveShareable(txn, c.getId(), f);
-				}
-				if (removeFromList(txn, g, SHARED_BY_US, f)) {
-					leaveShareable(txn, c.getId(), f);
-				}
-				if (removeFromList(txn, g, SHARED_WITH_US, f)) {
-					leaveShareable(txn, c.getId(), f);
-				}
+				// Look up the session for the contact, if there is one
+				GroupId contactGroupId = getContactGroup(c).getId();
+				StoredSession ss = getSession(txn, contactGroupId, sessionId);
+				if (ss == null) continue; // No session for this contact
+				// Let the engine perform a LEAVE action
+				Session session = sessionParser
+						.parseSession(contactGroupId, ss.bdfSession);
+				session = engine.onLeaveAction(txn, session);
+				// Store the updated session
+				storeSession(txn, ss.storageId, session);
 			}
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
 		}
 	}
 
-	private void checkForRaceCondition(Transaction txn, S f, Contact c)
-			throws FormatException, DbException {
+	private static class StoredSession {
 
-		GroupId contactGroup = getContactGroup(c).getId();
-		if (!listContains(txn, contactGroup, f.getId(), TO_BE_SHARED_BY_US))
-			// no race-condition, this invitation is invalid
-			throw new FormatException();
+		private final MessageId storageId;
+		private final BdfDictionary bdfSession;
 
-		// we have an invitation race condition
-		LocalAuthor author = db.getLocalAuthor(txn, c.getLocalAuthorId());
-		Bytes ourKey = new Bytes(author.getPublicKey());
-		Bytes theirKey = new Bytes(c.getAuthor().getPublicKey());
-
-		// determine which invitation takes precedence
-		boolean alice = ourKey.compareTo(theirKey) < 0;
-
-		if (alice) {
-			// our own invitation takes precedence, so just delete Bob's
-			LOG.info(
-					"Invitation race-condition: We are Alice deleting Bob's invitation.");
-			throw new FormatException();
-		} else {
-			// we are Bob, so we need to "take back" our own invitation
-			LOG.info(
-					"Invitation race-condition: We are Bob taking back our invitation.");
-			SharingSessionState state =
-					getSessionStateForLeaving(txn, f, c.getId());
-			if (state instanceof SharerSessionState) {
-				//SharerEngine engine = new SharerEngine();
-				//processSharerStateUpdate(txn, null,
-				//		engine.onLocalAction((SharerSessionState) state,
-				//				Action.LOCAL_LEAVE));
-
-				// simply remove from list instead of involving engine
-				removeFromList(txn, contactGroup, TO_BE_SHARED_BY_US, f);
-				// TODO here we could also remove the old session state
-				//      and invitation message
-			}
+		private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
+			this.storageId = storageId;
+			this.bdfSession = bdfSession;
 		}
-
-	}
-
-	private SS initializeSharerState(Transaction txn, S f,
-			ContactId contactId) throws FormatException, DbException {
-
-		Contact c = db.getContact(txn, contactId);
-		Group group = getContactGroup(c);
-
-		// create local message to keep engine state
-		long now = clock.currentTimeMillis();
-		Bytes salt = new Bytes(new byte[SHARING_SALT_LENGTH]);
-		random.nextBytes(salt.getBytes());
-		Message m = clientHelper.createMessage(localGroup.getId(), now,
-				BdfList.of(salt));
-		SessionId sessionId = new SessionId(m.getId().getBytes());
-
-		SS s = getSSFactory().build(sessionId, m.getId(), group.getId(),
-				SharerSessionState.State.PREPARE_INVITATION, contactId, f);
-
-		// save local state to database
-		BdfDictionary d = s.toBdfDictionary();
-		clientHelper.addLocalMessage(txn, m, d, false);
-
-		return s;
-	}
-
-	private IS initializeInviteeState(Transaction txn,
-			ContactId contactId, I msg, MessageId id)
-			throws FormatException, DbException {
-
-		Contact c = db.getContact(txn, contactId);
-		Group group = getContactGroup(c);
-		S f = getSFactory().parse(msg);
-
-		// create local message to keep engine state
-		long now = clock.currentTimeMillis();
-		Bytes mSalt = new Bytes(new byte[SHARING_SALT_LENGTH]);
-		random.nextBytes(mSalt.getBytes());
-		Message m = clientHelper.createMessage(localGroup.getId(), now,
-				BdfList.of(mSalt));
-
-		IS s = getISFactory()
-				.build(msg.getSessionId(), m.getId(), group.getId(),
-						InviteeSessionState.State.AWAIT_INVITATION, contactId,
-						f, id);
-
-		// save local state to database
-		BdfDictionary d = s.toBdfDictionary();
-		clientHelper.addLocalMessage(txn, m, d, false);
-
-		return s;
-	}
-
-	private SharingSessionState getSessionState(Transaction txn,
-			SessionId sessionId, boolean warn)
-			throws DbException, FormatException {
-
-		try {
-			return getSessionStateForSharer(txn, sessionId);
-		} catch (NoSuchMessageException e) {
-			// State not found directly, so query for state for invitee
-			BdfDictionary query = BdfDictionary.of(
-					new BdfEntry(SESSION_ID, sessionId)
-			);
-
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn, localGroup.getId(),
-							query);
-
-			if (map.size() > 1 && LOG.isLoggable(WARNING)) {
-				LOG.warning(
-						"More than one session state found for message with session ID " +
-								Arrays.hashCode(sessionId.getBytes()));
-			}
-			if (map.isEmpty()) {
-				if (warn && LOG.isLoggable(WARNING)) {
-					LOG.warning(
-							"No session state found for message with session ID " +
-									Arrays.hashCode(sessionId.getBytes()));
-				}
-				throw new FormatException();
-			}
-			return SharingSessionState
-					.fromBdfDictionary(getISFactory(), getSSFactory(),
-							map.values().iterator().next());
-		}
-	}
-
-	private SS getSessionStateForSharer(Transaction txn,
-			SessionId sessionId)
-			throws DbException, FormatException {
-
-		// we should be able to get the sharer state directly from sessionId
-		MessageId storageId = new MessageId(sessionId.getBytes());
-		BdfDictionary d =
-				clientHelper.getMessageMetadataAsDictionary(txn, storageId);
-
-		if (!d.getBoolean(IS_SHARER)) throw new FormatException();
-
-		return (SS) SharingSessionState
-				.fromBdfDictionary(getISFactory(), getSSFactory(), d);
-	}
-
-	private IS getSessionStateForResponse(Transaction txn,
-			S f, Contact c) throws DbException, FormatException {
-
-		// query for invitee states for that shareable in state await response
-		BdfDictionary query = BdfDictionary.of(
-				new BdfEntry(IS_SHARER, false),
-				new BdfEntry(CONTACT_ID, c.getId().getInt()),
-				new BdfEntry(SHAREABLE_ID, f.getId()),
-				new BdfEntry(STATE, AWAIT_LOCAL_RESPONSE.getValue())
-		);
-
-		Map<MessageId, BdfDictionary> map = clientHelper
-				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
-
-		if (map.size() > 1 && LOG.isLoggable(WARNING)) {
-			LOG.warning(
-					"More than one session state found for shareable with ID " +
-							Arrays.hashCode(f.getId().getBytes()) +
-							" in state AWAIT_LOCAL_RESPONSE for contact " +
-							c.getAuthor().getName());
-		}
-		if (map.isEmpty()) {
-			if (LOG.isLoggable(WARNING)) {
-				LOG.warning(
-						"No session state found for shareable with ID " +
-								Arrays.hashCode(f.getId().getBytes()) +
-								" in state AWAIT_LOCAL_RESPONSE");
-			}
-			throw new DbException();
-		}
-		return (IS) SharingSessionState
-				.fromBdfDictionary(getISFactory(), getSSFactory(),
-						map.values().iterator().next());
-	}
-
-	private SharingSessionState getSessionStateForLeaving(Transaction txn,
-			S f, ContactId c) throws DbException, FormatException {
-
-		BdfDictionary query = BdfDictionary.of(
-				new BdfEntry(CONTACT_ID, c.getInt()),
-				new BdfEntry(SHAREABLE_ID, f.getId())
-		);
-		Map<MessageId, BdfDictionary> map = clientHelper
-				.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
-		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
-			BdfDictionary d = m.getValue();
-			try {
-				SharingSessionState s = SharingSessionState
-						.fromBdfDictionary(getISFactory(), getSSFactory(), d);
-
-				// check that a shareable get be left in current session
-				if (s instanceof SharerSessionState) {
-					SharerSessionState state = (SharerSessionState) s;
-					SharerSessionState.State nextState =
-							state.getState()
-									.next(SharerSessionState.Action.LOCAL_LEAVE);
-					if (nextState != SharerSessionState.State.ERROR) {
-						return state;
-					}
-				} else {
-					InviteeSessionState state = (InviteeSessionState) s;
-					InviteeSessionState.State nextState = state.getState()
-							.next(InviteeSessionState.Action.LOCAL_LEAVE);
-					if (nextState != InviteeSessionState.State.ERROR) {
-						return state;
-					}
-				}
-			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-		throw new FormatException();
-	}
-
-	private void processStateUpdate(Transaction txn,
-			@Nullable MessageId messageId,
-			StateUpdate<SharingSessionState, BaseMessage> result, S f)
-			throws DbException, FormatException {
-
-		// perform actions based on new local state
-		performTasks(txn, result.localState, f);
-
-		// save new local state
-		MessageId storageId = result.localState.getStorageId();
-		clientHelper.mergeMessageMetadata(txn, storageId,
-				result.localState.toBdfDictionary());
-
-		// send messages
-		for (BaseMessage msg : result.toSend) {
-			sendMessage(txn, msg);
-		}
-
-		// broadcast events
-		for (Event event : result.toBroadcast) {
-			txn.attach(event);
-		}
-
-		// delete message
-		if (result.deleteMessage && messageId != null) {
-			if (LOG.isLoggable(INFO)) {
-				LOG.info("Deleting message with id " + messageId.hashCode());
-			}
-			db.deleteMessage(txn, messageId);
-			db.deleteMessageMetadata(txn, messageId);
-		}
-	}
-
-	private void processSharerStateUpdate(Transaction txn,
-			@Nullable MessageId messageId, StateUpdate<SS, BaseMessage> result)
-			throws DbException, FormatException {
-
-		StateUpdate<SharingSessionState, BaseMessage> r =
-				new StateUpdate<SharingSessionState, BaseMessage>(
-						result.deleteMessage, result.deleteState,
-						result.localState, result.toSend, result.toBroadcast);
-
-		// get shareable for later
-		S f = getSFactory().parse(result.localState);
-
-		processStateUpdate(txn, messageId, r, f);
-	}
-
-	private void processInviteeStateUpdate(Transaction txn,
-			@Nullable MessageId messageId, StateUpdate<IS, BaseMessage> result)
-			throws DbException, FormatException {
-
-		StateUpdate<SharingSessionState, BaseMessage> r =
-				new StateUpdate<SharingSessionState, BaseMessage>(
-						result.deleteMessage, result.deleteState,
-						result.localState, result.toSend, result.toBroadcast);
-
-		// get shareable for later
-		S f = getSFactory().parse(result.localState);
-
-		processStateUpdate(txn, messageId, r, f);
-	}
-
-	private void performTasks(Transaction txn, SharingSessionState localState,
-			S f) throws FormatException, DbException {
-
-		if (localState.getTask() == -1) return;
-
-		// remember task and remove it from localState
-		long task = localState.getTask();
-		localState.setTask(-1);
-
-		// get group ID for later
-		GroupId groupId = localState.getContactGroupId();
-		// get contact ID for later
-		ContactId contactId = localState.getContactId();
-
-		// perform tasks
-		if (task == TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US) {
-			addToList(txn, groupId, SHARED_WITH_US, f);
-		} else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US) {
-			removeFromList(txn, groupId, SHARED_WITH_US, f);
-		} else if (task == TASK_ADD_SHARED_SHAREABLE) {
-			// TODO we might want to call the add() method of the respective
-			//      manager here, because blogs add a description for example
-			db.addGroup(txn, f.getGroup());
-			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
-		} else if (task == TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US) {
-			addToList(txn, groupId, TO_BE_SHARED_BY_US, f);
-		} else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US) {
-			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
-		} else if (task == TASK_SHARE_SHAREABLE) {
-			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
-			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
-			addToList(txn, groupId, SHARED_BY_US, f);
-		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) {
-			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
-			removeFromList(txn, groupId, SHARED_BY_US, f);
-			// broadcast event informing UI that contact has left the group
-			ContactLeftShareableEvent
-					e = new ContactLeftShareableEvent(f.getId(), contactId);
-			txn.attach(e);
-		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) {
-			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
-			removeFromList(txn, groupId, SHARED_WITH_US, f);
-			// broadcast event informing UI that contact has left the group
-			ContactLeftShareableEvent
-					e = new ContactLeftShareableEvent(f.getId(), contactId);
-			txn.attach(e);
-		}
-	}
-
-	private void sendMessage(Transaction txn, BaseMessage m)
-			throws FormatException, DbException {
-
-		byte[] body = clientHelper.toByteArray(m.toBdfList());
-		Group group = db.getGroup(txn, m.getGroupId());
-
-		// add message itself as metadata
-		BdfDictionary d = m.toBdfDictionary();
-		d.put(LOCAL, true);
-		d.put(TIME, m.getTime());
-		Metadata meta = metadataEncoder.encode(d);
-
-		messageQueueManager
-				.sendMessage(txn, group, m.getTime(), body, meta);
-	}
-
-	@Override
-	public Group getContactGroup(Contact c) {
-		return contactGroupFactory.createContactGroup(getClientId(), c);
-	}
-
-	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
-			throws DbException, FormatException {
-		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
-				contactGroupId);
-		return new ContactId(meta.getLong(CONTACT_ID).intValue());
-	}
-
-	private void leaveShareable(Transaction txn, ContactId c, S f)
-			throws DbException, FormatException {
-
-		SharingSessionState state = getSessionStateForLeaving(txn, f, c);
-		if (state instanceof SharerSessionState) {
-			SharerSessionState.Action action =
-					SharerSessionState.Action.LOCAL_LEAVE;
-			SharerEngine<I, SS, IRR> engine =
-					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory(), clock);
-			processSharerStateUpdate(txn, null,
-					engine.onLocalAction((SS) state, action));
-		} else {
-			InviteeSessionState.Action action =
-					InviteeSessionState.Action.LOCAL_LEAVE;
-			InviteeEngine<IS, IR> engine =
-					new InviteeEngine<IS, IR>(getIRFactory(), clock);
-			processInviteeStateUpdate(txn, null,
-					engine.onLocalAction((IS) state, action));
-		}
-	}
-
-	private boolean listContains(Transaction txn, GroupId contactGroup,
-			GroupId shareable, String key) throws DbException, FormatException {
-
-		List<S> list = getShareableList(txn, contactGroup, key);
-		for (S f : list) {
-			if (f.getId().equals(shareable)) return true;
-		}
-		return false;
-	}
-
-	private boolean addToList(Transaction txn, GroupId groupId, String key,
-			S f) throws DbException, FormatException {
-
-		List<S> shareables = getShareableList(txn, groupId, key);
-		if (shareables.contains(f)) return false;
-		shareables.add(f);
-		storeShareableList(txn, groupId, key, shareables);
-		return true;
-	}
-
-	private boolean removeFromList(Transaction txn, GroupId groupId, String key,
-			S f) throws DbException, FormatException {
-
-		List<S> shareables = getShareableList(txn, groupId, key);
-		if (shareables.remove(f)) {
-			storeShareableList(txn, groupId, key, shareables);
-			return true;
-		}
-		return false;
-	}
-
-	private List<S> getShareableList(Transaction txn, GroupId groupId,
-			String key) throws DbException, FormatException {
-
-		BdfDictionary metadata =
-				clientHelper.getGroupMetadataAsDictionary(txn, groupId);
-		BdfList list = metadata.getList(key);
-
-		return parseShareableList(list);
-	}
-
-	private void storeShareableList(Transaction txn, GroupId groupId,
-			String key,
-			List<S> shareables) throws DbException, FormatException {
-
-		BdfList list = encodeShareableList(shareables);
-		BdfDictionary metadata = BdfDictionary.of(
-				new BdfEntry(key, list)
-		);
-		clientHelper.mergeGroupMetadata(txn, groupId, metadata);
-	}
-
-	private BdfList encodeShareableList(List<S> shareables) {
-		BdfList shareableList = new BdfList();
-		for (S f : shareables)
-			shareableList.add(getSFactory().encode(f));
-		return shareableList;
-	}
-
-	private List<S> parseShareableList(BdfList list) throws FormatException {
-		List<S> shareables = new ArrayList<S>(list.size());
-		for (int i = 0; i < list.size(); i++) {
-			BdfList shareable = list.getList(i);
-			shareables.add(getSFactory().parse(shareable));
-		}
-		return shareables;
-	}
-
-	private void deleteMessage(Transaction txn, MessageId messageId)
-			throws DbException {
-
-		if (LOG.isLoggable(INFO))
-			LOG.info("Deleting message with ID: " + messageId.hashCode());
-
-		db.deleteMessage(txn, messageId);
-		db.deleteMessageMetadata(txn, messageId);
 	}
 
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d18579e050e284a82869f78c81d77876c4169b0
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java
@@ -0,0 +1,50 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class SharingMessage {
+
+	private final MessageId id;
+	private final GroupId contactGroupId, shareableId;
+	private final long timestamp;
+	@Nullable
+	private final MessageId previousMessageId;
+
+	SharingMessage(MessageId id, GroupId contactGroupId, GroupId shareableId,
+			long timestamp, @Nullable MessageId previousMessageId) {
+		this.id = id;
+		this.previousMessageId = previousMessageId;
+		this.contactGroupId = contactGroupId;
+		this.shareableId = shareableId;
+		this.timestamp = timestamp;
+	}
+
+	MessageId getId() {
+		return id;
+	}
+
+	GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	GroupId getShareableId() {
+		return shareableId;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	@Nullable
+	public MessageId getPreviousMessageId() {
+		return previousMessageId;
+	}
+
+}
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 f56e6b8ca88b99279002853e3508a9390ac8673e..e4a24c1b73b1379b6eca6b36ef72e0a8c2c0570b 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
@@ -4,10 +4,13 @@ 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.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.blog.BlogSharingManager;
 import org.briarproject.briar.api.client.MessageQueueManager;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
 import org.briarproject.briar.api.forum.ForumManager;
 import org.briarproject.briar.api.forum.ForumSharingManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
@@ -68,14 +71,15 @@ public class SharingModule {
 	@Provides
 	@Singleton
 	ForumSharingValidator provideForumSharingValidator(
-			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-
+			ValidationManager validationManager, MessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock, ForumFactory forumFactory) {
 		ForumSharingValidator validator =
-				new ForumSharingValidator(clientHelper, metadataEncoder, clock);
-		messageQueueManager.registerMessageValidator(
-				ForumSharingManager.CLIENT_ID, validator);
-
+				new ForumSharingValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock, forumFactory);
+		validationManager
+				.registerMessageValidator(ForumSharingManager.CLIENT_ID,
+						validator);
 		return validator;
 	}
 
@@ -83,14 +87,14 @@ public class SharingModule {
 	@Singleton
 	ForumSharingManager provideForumSharingManager(
 			LifecycleManager lifecycleManager, ContactManager contactManager,
-			MessageQueueManager messageQueueManager,
+			ValidationManager validationManager,
 			ConversationManager conversationManager, ForumManager forumManager,
 			ForumSharingManagerImpl forumSharingManager) {
 
 		lifecycleManager.registerClient(forumSharingManager);
 		contactManager.registerAddContactHook(forumSharingManager);
 		contactManager.registerRemoveContactHook(forumSharingManager);
-		messageQueueManager.registerIncomingMessageHook(
+		validationManager.registerIncomingMessageHook(
 				ForumSharingManager.CLIENT_ID, forumSharingManager);
 		conversationManager.registerConversationClient(forumSharingManager);
 		forumManager.registerRemoveForumHook(forumSharingManager);
@@ -98,4 +102,37 @@ public class SharingModule {
 		return forumSharingManager;
 	}
 
+	@Provides
+	MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
+	@Provides
+	MessageParser<Forum> provideForumMessageParser(
+			ForumMessageParserImpl forumMessageParser) {
+		return forumMessageParser;
+	}
+
+	@Provides
+	SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
+		return sessionEncoder;
+	}
+
+	@Provides
+	SessionParser provideSessionParser(SessionParserImpl sessionParser) {
+		return sessionParser;
+	}
+
+	@Provides
+	ProtocolEngine<Forum> provideForumProtocolEngine(
+			ForumProtocolEngineImpl forumProtocolEngine) {
+		return forumProtocolEngine;
+	}
+
+	@Provides
+	InvitationFactory<Forum> provideForumInvitationFactory(
+			ForumInvitationFactoryImpl forumInvitationFactory) {
+		return forumInvitationFactory;
+	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java
index 791ca044618283be64260b81d10330f0a2f30c91..77ddbd52c4a5ab780a9f76bbae030abf7b05bcfb 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java
@@ -18,6 +18,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID;
 import static org.briarproject.briar.api.sharing.SharingConstants.STATE;
 import static org.briarproject.briar.api.sharing.SharingConstants.STORAGE_ID;
 
+@Deprecated
 @NotThreadSafe
 @NotNullByDefault
 abstract class SharingSessionState {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8f9287f8f43162acd224d6ce0dc43dcc78c7fb3
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java
@@ -0,0 +1,101 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.UniqueId;
+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.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+
+import java.util.Collections;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+
+@Immutable
+@NotNullByDefault
+abstract class SharingValidator extends BdfMessageValidator {
+
+	private final MessageEncoder messageEncoder;
+
+	SharingValidator(MessageEncoder messageEncoder, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+		this.messageEncoder = messageEncoder;
+	}
+
+	@Override
+	protected BdfMessageContext validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
+		switch (type) {
+			case INVITE:
+				return validateInviteMessage(m, body);
+			case ACCEPT:
+			case DECLINE:
+			case LEAVE:
+			case ABORT:
+				return validateNonInviteMessage(type, m, body);
+			default:
+				throw new FormatException();
+		}
+	}
+
+	private BdfMessageContext validateInviteMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 4);
+		byte[] previousMessageId = body.getOptionalRaw(1);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+		BdfList descriptor = body.getList(2);
+		GroupId shareableId = validateDescriptor(descriptor);
+		String msg = body.getOptionalString(3);
+		checkLength(msg, 1, MAX_INVITATION_MESSAGE_LENGTH);
+
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INVITE, shareableId, m.getTimestamp(), false,
+						false, false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	protected abstract GroupId validateDescriptor(BdfList descriptor)
+			throws FormatException;
+
+	private BdfMessageContext validateNonInviteMessage(MessageType type,
+			Message m, BdfList body) throws FormatException {
+		checkSize(body, 3);
+		byte[] shareableId = body.getRaw(1);
+		checkLength(shareableId, UniqueId.LENGTH);
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, new GroupId(shareableId),
+						m.getTimestamp(), false, false, false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..e4ba53501f7da275fb51c67b0a53418f7f718429
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/State.java
@@ -0,0 +1,34 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum State {
+
+	START(0), LOCAL_INVITED(1), REMOTE_INVITED(2), SHARING(3), LOCAL_LEFT(4),
+	REMOTE_LEFT(5),	REMOTE_HANGING(6), ERROR(7);
+
+	private final int value;
+
+	State(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public boolean canInvite() {
+		return this == START || this == REMOTE_LEFT;
+	}
+
+	static State fromValue(int value) throws FormatException {
+		for (State s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
index 9ca8805f20819998ad4df22c41a798315d10e19e..30567fdf225431c37ef54d27fc1908c2d2e7c9e1 100644
--- a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java
@@ -45,7 +45,8 @@ public class ForumManagerTest
 		forum0 = forumManager0.addForum("Test Forum");
 		groupId0 = forum0.getId();
 		// share forum
-		forumSharingManager0.sendInvitation(groupId0, contactId1From0, null);
+		forumSharingManager0.sendInvitation(groupId0, contactId1From0, null,
+				clock.currentTimeMillis());
 		sync0To1(1, true);
 		forumSharingManager1.respondToInvitation(forum0, contact0From1, true);
 		sync1To0(1, true);
@@ -189,7 +190,8 @@ public class ForumManagerTest
 		// share a second forum
 		Forum forum1 = forumManager0.addForum("Test Forum1");
 		GroupId g1 = forum1.getId();
-		forumSharingManager0.sendInvitation(g1, contactId1From0, null);
+		forumSharingManager0.sendInvitation(g1, contactId1From0, null,
+				clock.currentTimeMillis());
 		sync0To1(1, true);
 		forumSharingManager1.respondToInvitation(forum1, contact0From1, true);
 		sync1To0(1, true);
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
index 7431ffe483f160022bbf03d1ea51507a3e8b5c45..fe4142331256902e70f9f581ff1098aa98f30bc0 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java
@@ -12,6 +12,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
 import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
 import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
 import org.briarproject.briar.api.sharing.InvitationMessage;
+import org.briarproject.briar.api.sharing.InvitationResponse;
 import org.briarproject.briar.test.BriarIntegrationTest;
 import org.briarproject.briar.test.BriarIntegrationTestComponent;
 import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
@@ -97,10 +98,10 @@ public class GroupInvitationIntegrationTest
 		GroupInvitationRequest request =
 				(GroupInvitationRequest) messages.iterator().next();
 		assertEquals(msg, request.getMessage());
-		assertEquals(author0, request.getCreator());
+		assertEquals(author0, request.getShareable().getCreator());
 		assertEquals(timestamp, request.getTimestamp());
 		assertEquals(contactId0From1, request.getContactId());
-		assertEquals(privateGroup0.getName(), request.getGroupName());
+		assertEquals(privateGroup0.getName(), request.getShareable().getName());
 		assertFalse(request.isLocal());
 		assertFalse(request.isRead());
 	}
@@ -123,7 +124,7 @@ public class GroupInvitationIntegrationTest
 		for (InvitationMessage m : messages) {
 			if (m instanceof GroupInvitationResponse) {
 				foundResponse = true;
-				GroupInvitationResponse response = (GroupInvitationResponse) m;
+				InvitationResponse response = (GroupInvitationResponse) m;
 				assertEquals(contactId0From1, response.getContactId());
 				assertTrue(response.isLocal());
 				assertFalse(response.wasAccepted());
@@ -140,7 +141,7 @@ public class GroupInvitationIntegrationTest
 		for (InvitationMessage m : messages) {
 			if (m instanceof GroupInvitationResponse) {
 				foundResponse = true;
-				GroupInvitationResponse response = (GroupInvitationResponse) m;
+				InvitationResponse response = (GroupInvitationResponse) m;
 				assertEquals(contactId0From1, response.getContactId());
 				assertFalse(response.isLocal());
 				assertFalse(response.wasAccepted());
@@ -172,7 +173,7 @@ public class GroupInvitationIntegrationTest
 		for (InvitationMessage m : messages) {
 			if (m instanceof GroupInvitationResponse) {
 				foundResponse = true;
-				GroupInvitationResponse response = (GroupInvitationResponse) m;
+				InvitationResponse response = (GroupInvitationResponse) m;
 				assertTrue(response.wasAccepted());
 			}
 		}
@@ -187,7 +188,7 @@ public class GroupInvitationIntegrationTest
 		for (InvitationMessage m : messages) {
 			if (m instanceof GroupInvitationResponse) {
 				foundResponse = true;
-				GroupInvitationResponse response = (GroupInvitationResponse) m;
+				InvitationResponse response = (GroupInvitationResponse) m;
 				assertTrue(response.wasAccepted());
 			}
 		}
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 620775b541444874f9276567a926a8a682f88354..37c7b76e321a1e89a0d9ceb7c105499465dd5d31 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
@@ -90,8 +90,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 
 	private final GroupInvitationManagerImpl groupInvitationManager;
 
-	private final Group localGroup =
-			new Group(new GroupId(getRandomId()), CLIENT_ID, getRandomBytes(5));
 	private final Transaction txn = new Transaction(null, false);
 	private final ContactId contactId = new ContactId(0);
 	private final Author author =
@@ -141,8 +139,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			will(returnValue(inviteeEngine));
 			oneOf(engineFactory).createPeerEngine();
 			will(returnValue(peerEngine));
-			oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID);
-			will(returnValue(localGroup));
 		}});
 		MetadataParser metadataParser = context.mock(MetadataParser.class);
 		MessageTracker messageTracker = context.mock(MessageTracker.class);
@@ -156,7 +152,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 	@Test
 	public void testCreateLocalState() throws Exception {
 		context.checking(new Expectations() {{
-			oneOf(db).addGroup(txn, localGroup);
 			oneOf(db).getContacts(txn);
 			will(returnValue(Collections.singletonList(contact)));
 		}});
@@ -651,6 +646,9 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 				new InviteMessage(message.getId(), contactGroup.getId(),
 						privateGroup.getId(), time1, "name", author,
 						new byte[0], null, new byte[0]);
+		final PrivateGroup pg =
+				new PrivateGroup(privateGroup, invite.getGroupName(),
+						invite.getCreator(), invite.getSalt());
 
 		context.checking(new Expectations() {{
 			oneOf(db).startTransaction(true);
@@ -668,12 +666,11 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 			oneOf(messageParser).parseMetadata(meta);
 			will(returnValue(messageMetadata1));
 			oneOf(db).getMessageStatus(txn, contactId, message.getId());
-			oneOf(clientHelper).getMessage(txn, message.getId());
-			will(returnValue(message));
-			oneOf(clientHelper).toList(message);
-			will(returnValue(body));
-			oneOf(messageParser).parseInviteMessage(message, body);
+			oneOf(messageParser).getInviteMessage(txn, message.getId());
 			will(returnValue(invite));
+			oneOf(privateGroupFactory).createPrivateGroup(invite.getGroupName(),
+					invite.getCreator(), invite.getSalt());
+			will(returnValue(pg));
 			oneOf(db).containsGroup(txn, privateGroup.getId());
 			will(returnValue(true));
 			// second message
@@ -742,21 +739,13 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 					contactGroup.getId(), query);
 			will(returnValue(results));
 			// message 1
-			oneOf(clientHelper).getMessage(txn, message.getId());
-			will(returnValue(message));
-			oneOf(clientHelper).toList(message);
-			will(returnValue(body));
-			oneOf(messageParser).parseInviteMessage(message, body);
+			oneOf(messageParser).getInviteMessage(txn, message.getId());
 			will(returnValue(inviteMessage1));
 			oneOf(privateGroupFactory).createPrivateGroup(groupName, author,
 					salt);
 			will(returnValue(pg));
 			// message 2
-			oneOf(clientHelper).getMessage(txn, messageId2);
-			will(returnValue(message2));
-			oneOf(clientHelper).toList(message2);
-			will(returnValue(body2));
-			oneOf(messageParser).parseInviteMessage(message2, body2);
+			oneOf(messageParser).getInviteMessage(txn, messageId2);
 			will(returnValue(inviteMessage2));
 			oneOf(privateGroupFactory).createPrivateGroup(groupName, author,
 					salt);
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 2e4c64ddb195b12e99cd89f3793cc0b417b61e1c..6e427277087c8550dfedfc14031106dd2b3f2ea4 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
@@ -144,11 +144,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 		expectSendJoinMessage(properJoinMessage, true);
 		context.checking(new Expectations() {{
 			oneOf(messageTracker).trackOutgoingMessage(txn, message);
-			oneOf(clientHelper).getMessage(txn, lastRemoteMessageId);
-			will(returnValue(inviteMsg));
-			oneOf(clientHelper).toList(inviteMsg);
-			will(returnValue(inviteList));
-			oneOf(messageParser).parseInviteMessage(inviteMsg, inviteList);
+			oneOf(messageParser).getInviteMessage(txn, lastRemoteMessageId);
 			will(returnValue(inviteMessage));
 			oneOf(privateGroupFactory)
 					.createPrivateGroup(inviteMessage.getGroupName(),
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 e5e65f94f9256cc30cf72e86b1bdf548235b1638..67b1a37614252402e66bf4488521b73b7187094a 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
@@ -107,7 +107,8 @@ public class BlogSharingIntegrationTest
 
 		// create invitation
 		blogSharingManager0
-				.sendInvitation(blog1.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog1.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync invitation
 		sync0To1(1, false);
@@ -122,7 +123,8 @@ public class BlogSharingIntegrationTest
 
 		// send invitation
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog2.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// invitee has own blog and that of the sharer
 		assertEquals(2, blogManager1.getBlogs().size());
@@ -194,7 +196,8 @@ public class BlogSharingIntegrationTest
 
 		// send invitation
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, null);
+				.sendInvitation(blog2.getId(), contactId1From0, null,
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -251,7 +254,8 @@ public class BlogSharingIntegrationTest
 
 		// send invitation
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog2.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -313,7 +317,8 @@ public class BlogSharingIntegrationTest
 
 		// sharer sends invitation for 2's blog to 1
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog2.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -349,7 +354,8 @@ public class BlogSharingIntegrationTest
 
 		// send invitation
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog2.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -399,7 +405,8 @@ public class BlogSharingIntegrationTest
 
 		// sharer sends invitation for 2's blog to 1
 		blogSharingManager0
-				.sendInvitation(blog2.getId(), contactId1From0, "Hi!");
+				.sendInvitation(blog2.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
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 72c0156b20f614d8ef8960743fe01840436a5641..3452025b9d3acacb709622b61b3971b2f3e82760 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
@@ -2,19 +2,14 @@ package org.briarproject.briar.sharing;
 
 import net.jodah.concurrentunit.Waiter;
 
-import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.data.BdfList;
 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.event.Event;
 import org.briarproject.bramble.api.event.EventListener;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.test.TestDatabaseModule;
-import org.briarproject.briar.api.client.MessageQueueManager;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.forum.Forum;
 import org.briarproject.briar.api.forum.ForumInvitationRequest;
 import org.briarproject.briar.api.forum.ForumInvitationResponse;
@@ -39,11 +34,7 @@ import java.util.Collection;
 import java.util.List;
 
 import static junit.framework.Assert.assertNotNull;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomString;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH;
-import static org.briarproject.briar.api.forum.ForumSharingManager.CLIENT_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -115,7 +106,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -171,7 +163,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, null);
+				.sendInvitation(forum0.getId(), contactId1From0, null,
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -227,7 +220,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -245,9 +239,8 @@ public class ForumSharingIntegrationTest
 		assertTrue(forumManager1.getForums().contains(forum0));
 
 		// sharer shares forum with invitee
-		Contact c1 = contactManager0.getContact(contactId1From0);
 		assertTrue(forumSharingManager0.getSharedWith(forum0.getId())
-				.contains(c1));
+				.contains(contact1From0));
 		// invitee gets forum shared by sharer
 		Contact contact0 = contactManager1.getContact(contactId1From0);
 		assertTrue(forumSharingManager1.getSharedWith(forum0.getId())
@@ -265,14 +258,16 @@ public class ForumSharingIntegrationTest
 
 		// sharer no longer shares forum with invitee
 		assertFalse(forumSharingManager0.getSharedWith(forum0.getId())
-				.contains(c1));
+				.contains(contact1From0));
 		// invitee no longer gets forum shared by sharer
 		assertFalse(forumSharingManager1.getSharedWith(forum0.getId())
 				.contains(contact0));
-		// forum can be shared again
-		assertTrue(forumSharingManager0.canBeShared(forum0.getId(), c1));
-		Contact c0 = contactManager1.getContact(contactId0From1);
-		assertTrue(forumSharingManager1.canBeShared(forum0.getId(), c0));
+		// forum can be shared again by sharer
+		assertTrue(forumSharingManager0
+				.canBeShared(forum0.getId(), contact1From0));
+		// invitee that left can not share again
+		assertFalse(forumSharingManager1
+				.canBeShared(forum0.getId(), contact0From1));
 	}
 
 	@Test
@@ -282,7 +277,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, null);
+				.sendInvitation(forum0.getId(), contactId1From0, null,
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -336,7 +332,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, null);
+				.sendInvitation(forum0.getId(), contactId1From0, null,
+						clock.currentTimeMillis());
 
 		// sharer un-subscribes from forum
 		forumManager0.removeForum(forum0);
@@ -360,7 +357,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, null);
+				.sendInvitation(forum0.getId(), contactId1From0, null,
+						clock.currentTimeMillis());
 
 		// sharer un-subscribes from forum
 		forumManager0.removeForum(forum0);
@@ -375,73 +373,15 @@ public class ForumSharingIntegrationTest
 		assertEquals(1, forumManager1.getForums().size());
 	}
 
-	@Test
-	public void testSessionIdReuse() throws Exception {
-		// initialize and let invitee accept all requests
-		listenToEvents(true);
-
-		// send invitation
-		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
-
-		// sync first request message
-		sync0To1(1, true);
-		eventWaiter.await(TIMEOUT, 1);
-		assertTrue(listener1.requestReceived);
-
-		// sync response back
-		sync1To0(1, true);
-		eventWaiter.await(TIMEOUT, 1);
-		assertTrue(listener0.responseReceived);
-
-		// forum was added successfully
-		assertEquals(1, forumManager1.getForums().size());
-
-		// reset event received state
-		listener1.requestReceived = false;
-
-		// get SessionId from invitation
-		List<InvitationMessage> list = new ArrayList<InvitationMessage>(
-				forumSharingManager1
-						.getInvitationMessages(contactId0From1));
-		assertEquals(2, list.size());
-		InvitationMessage msg = list.get(0);
-		SessionId sessionId = msg.getSessionId();
-		assertEquals(sessionId, list.get(1).getSessionId());
-
-		// get all sorts of stuff needed to send a message
-		MessageQueueManager queue = c0.getMessageQueueManager();
-		Contact c1 = contactManager0.getContact(contactId1From0);
-		Group group = contactGroupFactory.createContactGroup(CLIENT_ID, c1);
-		long time = clock.currentTimeMillis();
-		BdfList bodyList =
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId.getBytes(),
-						getRandomString(42), getRandomBytes(FORUM_SALT_LENGTH));
-		byte[] body = clientHelper.toByteArray(bodyList);
-
-		// add the message to the queue
-		Transaction txn = db0.startTransaction(false);
-		try {
-			queue.sendMessage(txn, group, time, body, new Metadata());
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
-
-		// actually send the message
-		sync0To1(1, false);
-		// make sure there was no new request received
-		assertFalse(listener1.requestReceived);
-	}
-
-	@Test
+	@Test(expected = ProtocolStateException.class)
 	public void testSharingSameForumWithEachOther() throws Exception {
 		// initialize and let invitee accept all requests
 		listenToEvents(true);
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -455,24 +395,12 @@ public class ForumSharingIntegrationTest
 
 		// forum was added successfully
 		assertEquals(1, forumManager1.getForums().size());
-		assertEquals(2,
-				forumSharingManager0.getInvitationMessages(contactId1From0)
-						.size());
 
 		// invitee now shares same forum back
 		forumSharingManager1.sendInvitation(forum0.getId(),
 				contactId0From1,
-				"I am re-sharing this forum with you.");
-
-		// sync re-share invitation
-		sync1To0(1, false);
-
-		// make sure that no new request was received
-		assertFalse(listener0.requestReceived);
-		assertEquals(2,
-				forumSharingManager0.getInvitationMessages(contactId1From0)
-						.size());
-		assertEquals(0, forumSharingManager0.getInvitations().size());
+				"I am re-sharing this forum with you.",
+				clock.currentTimeMillis());
 	}
 
 	@Test
@@ -482,62 +410,48 @@ public class ForumSharingIntegrationTest
 
 		// invitee adds the same forum
 		Transaction txn = db1.startTransaction(false);
-		db1.addGroup(txn, forum0.getGroup());
+		forumManager1.addForum(txn, forum0);
 		db1.commitTransaction(txn);
 		db1.endTransaction(txn);
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// invitee now shares same forum back
 		forumSharingManager1.sendInvitation(forum0.getId(),
-				contactId0From1, "I am re-sharing this forum with you.");
+				contactId0From1, "I am re-sharing this forum with you.",
+				clock.currentTimeMillis());
 
-		// find out who should be Alice, because of random keys
-		Bytes key0 = new Bytes(author0.getPublicKey());
-		Bytes key1 = new Bytes(author1.getPublicKey());
+		// prevent automatic responses
+		respond = false;
 
 		// only now sync first request message
-		boolean alice = key1.compareTo(key0) < 0;
-		if (alice) {
-			sync0To1(1, false);
-			assertFalse(listener1.requestReceived);
-		} else {
-			sync0To1(1, true);
-			eventWaiter.await(TIMEOUT, 1);
-			assertTrue(listener1.requestReceived);
-		}
+		sync0To1(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener1.requestReceived);
 
-		// sync second invitation
-		alice = key0.compareTo(key1) < 0;
-		if (alice) {
-			sync1To0(1, false);
-			assertFalse(listener0.requestReceived);
-
-			// sharer did not receive request, but response to own request
-			eventWaiter.await(TIMEOUT, 1);
-			assertTrue(listener0.responseReceived);
-
-			assertEquals(2, forumSharingManager0
-					.getInvitationMessages(contactId1From0).size());
-			assertEquals(3, forumSharingManager1
-					.getInvitationMessages(contactId0From1).size());
-		} else {
-			sync1To0(1, true);
-			eventWaiter.await(TIMEOUT, 1);
-			assertTrue(listener0.requestReceived);
-
-			// send response from sharer to invitee and make sure it arrived
-			sync0To1(1, true);
-			eventWaiter.await(TIMEOUT, 1);
-			assertTrue(listener1.responseReceived);
-
-			assertEquals(3, forumSharingManager0
-					.getInvitationMessages(contactId1From0).size());
-			assertEquals(2, forumSharingManager1
-					.getInvitationMessages(contactId0From1).size());
-		}
+		// sync second invitation which counts as accept
+		sync1To0(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener0.requestReceived);
+
+		// both peers should share the forum with each other now
+		assertTrue(forumSharingManager0.getSharedWith(forum0.getId())
+				.contains(contact1From0));
+		assertTrue(forumSharingManager1.getSharedWith(forum0.getId())
+				.contains(contact0From1));
+
+		// and both have each other's invitations (and no response)
+		assertEquals(2, forumSharingManager0
+				.getInvitationMessages(contactId1From0).size());
+		assertEquals(2, forumSharingManager1
+				.getInvitationMessages(contactId0From1).size());
+
+		// there are no more open invitations
+		assertTrue(forumSharingManager0.getInvitations().isEmpty());
+		assertTrue(forumSharingManager1.getInvitations().isEmpty());
 	}
 
 	@Test
@@ -547,7 +461,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -564,18 +479,12 @@ public class ForumSharingIntegrationTest
 		assertEquals(1,
 				forumSharingManager0.getSharedWith(forum0.getId()).size());
 
-		// remember SessionId from invitation
-		List<InvitationMessage> list = new ArrayList<InvitationMessage>(
-				forumSharingManager1
-						.getInvitationMessages(contactId0From1));
-		assertEquals(2, list.size());
-		InvitationMessage msg = list.get(0);
-		SessionId sessionId = msg.getSessionId();
-		assertEquals(sessionId, list.get(1).getSessionId());
-
 		// contacts now remove each other
 		removeAllContacts();
 
+		// invitee still has forum
+		assertEquals(1, forumManager1.getForums().size());
+
 		// make sure sharer does share the forum with nobody now
 		assertEquals(0,
 				forumSharingManager0.getSharedWith(forum0.getId()).size());
@@ -584,35 +493,30 @@ public class ForumSharingIntegrationTest
 		addDefaultContacts();
 		addContacts1And2();
 
-		// get all sorts of stuff needed to send a message
-		MessageQueueManager queue = c0.getMessageQueueManager();
-		Contact c1 = contactManager0.getContact(contactId1From0);
-		Group group = contactGroupFactory.createContactGroup(CLIENT_ID, c1);
-		long time = clock.currentTimeMillis();
-
-		// construct a new message re-using the old SessionId
-		BdfList bodyList = BdfList.of(SHARE_MSG_TYPE_INVITATION,
-				sessionId.getBytes(),
-				getRandomString(42),
-				getRandomBytes(FORUM_SALT_LENGTH)
-		);
-		byte[] body = clientHelper.toByteArray(bodyList);
-
-		// add the message to the queue
-		Transaction txn = db0.startTransaction(false);
-		try {
-			queue.sendMessage(txn, group, time, body, new Metadata());
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
+		// forum can be shared with contacts again
+		assertTrue(forumSharingManager0
+				.canBeShared(forum0.getId(), contact1From0));
+		assertTrue(forumSharingManager0
+				.canBeShared(forum0.getId(), contact2From0));
+		// send invitation
+		forumSharingManager0
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
-		// actually send the message
+		// sync first request message
 		sync0To1(1, true);
 		eventWaiter.await(TIMEOUT, 1);
-		// make sure the new request was received with the same sessionId
-		// as proof that the state got deleted along with contacts
 		assertTrue(listener1.requestReceived);
+
+		// sync response back
+		sync1To0(1, true);
+		eventWaiter.await(TIMEOUT, 1);
+		assertTrue(listener0.responseReceived);
+
+		// forum is still there
+		assertEquals(1, forumManager1.getForums().size());
+		assertEquals(1,
+				forumSharingManager0.getSharedWith(forum0.getId()).size());
 	}
 
 	@Test
@@ -633,14 +537,16 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 		// sync first request message
 		sync0To1(1, true);
 
 		// second sharer sends invitation for same forum
 		assertTrue(contactId1From2 != null);
 		forumSharingManager2
-				.sendInvitation(forum0.getId(), contactId1From2, null);
+				.sendInvitation(forum0.getId(), contactId1From2, null,
+						clock.currentTimeMillis());
 		// sync second request message
 		sync2To1(1, true);
 
@@ -650,13 +556,6 @@ public class ForumSharingIntegrationTest
 		assertEquals(1, forums.size());
 		assertEquals(2, forums.iterator().next().getNewSharers().size());
 		assertEquals(forum0, forums.iterator().next().getShareable());
-		assertEquals(2,
-				forumSharingManager1.getSharedWith(forum0.getId()).size());
-
-		// make sure both sharers actually share the forum
-		Collection<Contact> contacts =
-				forumSharingManager1.getSharedWith(forum0.getId());
-		assertEquals(2, contacts.size());
 
 		// answer second request
 		assertNotNull(contactId2From1);
@@ -675,6 +574,11 @@ public class ForumSharingIntegrationTest
 		sync1To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.responseReceived);
+
+		// make sure both sharers actually share the forum
+		Collection<Contact> contacts =
+				forumSharingManager1.getSharedWith(forum0.getId());
+		assertEquals(2, contacts.size());
 	}
 
 	@Test
@@ -684,7 +588,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -745,7 +650,8 @@ public class ForumSharingIntegrationTest
 
 		// send invitation again
 		forumSharingManager0
-				.sendInvitation(forum0.getId(), contactId1From0, "Hi!");
+				.sendInvitation(forum0.getId(), contactId1From0, "Hi!",
+						clock.currentTimeMillis());
 
 		// sync first request message
 		sync0To1(1, true);
@@ -799,8 +705,10 @@ public class ForumSharingIntegrationTest
 				requestReceived = true;
 				Forum f = event.getShareable();
 				try {
-					Contact c = contactManager0.getContact(contactId1From0);
-					forumSharingManager0.respondToInvitation(f, c, true);
+					if (respond) {
+						Contact c = contactManager0.getContact(contactId1From0);
+						forumSharingManager0.respondToInvitation(f, c, true);
+					}
 				} catch (DbException ex) {
 					eventWaiter.rethrow(ex);
 				} finally {
@@ -814,7 +722,6 @@ public class ForumSharingIntegrationTest
 	private class InviteeListener implements EventListener {
 
 		private volatile boolean requestReceived = false;
-		private volatile boolean responseReceived = false;
 
 		private final boolean accept, answer;
 
@@ -836,13 +743,13 @@ public class ForumSharingIntegrationTest
 				if (!answer) return;
 				Forum f = event.getShareable();
 				try {
-					eventWaiter.assertEquals(1,
-							forumSharingManager1.getInvitations().size());
-					SharingInvitationItem invitation =
-							forumSharingManager1.getInvitations().iterator()
-									.next();
-					eventWaiter.assertEquals(f, invitation.getShareable());
 					if (respond) {
+						eventWaiter.assertEquals(1,
+								forumSharingManager1.getInvitations().size());
+						SharingInvitationItem invitation =
+								forumSharingManager1.getInvitations().iterator()
+										.next();
+						eventWaiter.assertEquals(f, invitation.getShareable());
 						Contact c =
 								contactManager1
 										.getContact(event.getContactId());
@@ -859,7 +766,6 @@ public class ForumSharingIntegrationTest
 				ForumInvitationResponseReceivedEvent event =
 						(ForumInvitationResponseReceivedEvent) e;
 				eventWaiter.assertEquals(contactId0From1, event.getContactId());
-				responseReceived = true;
 				eventWaiter.resume();
 			}
 		}
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
index 52887134cf3c0945aac3d2ef02a4d5bc1bdfb7b2..c7d2d98f26c03346953564585671a41d0f7f5c4a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
@@ -4,340 +4,314 @@ import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.client.BdfMessageContext;
 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.sync.MessageId;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.bramble.test.ValidatorTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
+import org.jmock.Expectations;
 import org.junit.Test;
 
+import java.util.Collection;
+
 import javax.annotation.Nullable;
 
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
 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.sharing.SharingConstants.INVITATION_MSG;
-import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
 import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
-import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class ForumSharingValidatorTest extends ValidatorTestCase {
 
-	private final SessionId sessionId = new SessionId(TestUtils.getRandomId());
+	private final MessageEncoder messageEncoder =
+			context.mock(MessageEncoder.class);
+	private final ForumFactory forumFactory = context.mock(ForumFactory.class);
+	private final ForumSharingValidator v =
+			new ForumSharingValidator(messageEncoder, clientHelper,
+					metadataEncoder, clock, forumFactory);
+
+	private final MessageId previousMsgId = new MessageId(getRandomId());
 	private final String forumName =
 			TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH);
 	private final byte[] salt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH);
+	private final Forum forum = new Forum(group, forumName, salt);
+	private final BdfList descriptor = BdfList.of(forumName, salt);
 	private final String content =
 			TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH);
+	private final BdfDictionary meta =
+			BdfDictionary.of(new BdfEntry("meta", "data"));
 
 	@Test
 	public void testAcceptsInvitationWithContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						content));
+		assertExpectedContext(messageContext, previousMsgId);
+	}
+
+	@Test
+	public void testAcceptsInvitationWithNullContent() throws Exception {
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, content));
-		assertExpectedContextForInvitation(messageContext, forumName, content);
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
-	public void testAcceptsInvitationWithoutContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+	public void testAcceptsInvitationWithNullPreviousMsgId() throws Exception {
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt));
-		assertExpectedContextForInvitation(messageContext, forumName, null);
+				BdfList.of(INVITE.getValue(), null, descriptor, null));
+		assertExpectedContext(messageContext, null);
 	}
 
 	@Test
 	public void testAcceptsAccept() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(ACCEPT);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ACCEPT, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ACCEPT);
+				BdfList.of(ACCEPT.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsDecline() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(DECLINE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_DECLINE, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_DECLINE);
+				BdfList.of(DECLINE.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsLeave() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(LEAVE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_LEAVE, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_LEAVE);
+				BdfList.of(LEAVE.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(ABORT);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ABORT);
+				BdfList.of(ABORT.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullMessageType() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of(null, sessionId));
+		v.validateMessage(message, group,
+				BdfList.of(null, groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonLongMessageType() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of("", sessionId));
+		v.validateMessage(message, group,
+				BdfList.of("", groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsInvalidMessageType() throws Exception {
-		int invalidMessageType = SHARE_MSG_TYPE_ABORT + 1;
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		int invalidMessageType = ABORT.getValue() + 1;
 		v.validateMessage(message, group,
-				BdfList.of(invalidMessageType, sessionId));
+				BdfList.of(invalidMessageType, groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullSessionId() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, null));
+				BdfList.of(ABORT.getValue(), null, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonRawSessionId() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, 123));
+		v.validateMessage(message, group, BdfList.of(ABORT.getValue(), 123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortSessionId() throws Exception {
-		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+				BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongSessionId() throws Exception {
-		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+				BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortBodyForAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of(SHARE_MSG_TYPE_ABORT));
+		v.validateMessage(message, group,
+				BdfList.of(ABORT.getValue(), groupId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongBodyForAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId, 123));
+				BdfList.of(ABORT.getValue(), groupId, previousMsgId, 123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortBodyForInvitation() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongBodyForInvitation() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, content, 123));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null,
+						123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(null, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, null,
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonStringForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(123, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, 123,
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of("", salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, "",
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test
 	public void testAcceptsMinLengthForumName() throws Exception {
 		String shortForumName = TestUtils.getRandomString(1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList validDescriptor = BdfList.of(shortForumName, salt);
+		expectCreateForum(shortForumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, shortForumName,
-						salt, content));
-		assertExpectedContextForInvitation(messageContext, shortForumName,
-				content);
+				BdfList.of(INVITE.getValue(), previousMsgId, validDescriptor,
+						null));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongForumName() throws Exception {
 		String invalidForumName =
 				TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(invalidForumName, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId,
-						invalidForumName, salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullSalt() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, null);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						null, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonRawSalt() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, 123);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						123, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortSalt() throws Exception {
 		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH - 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						invalidSalt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongSalt() throws Exception {
 		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						invalidSalt, content));
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsNullContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, null));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonStringContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, 123));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						123));
 	}
 
 	@Test
 	public void testAcceptsMinLengthContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, ""));
-		assertExpectedContextForInvitation(messageContext, forumName, "");
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, "1"));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongContent() throws Exception {
 		String invalidContent =
 				TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, invalidContent));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						invalidContent));
 	}
 
-	private void assertExpectedContextForInvitation(
-			BdfMessageContext messageContext, String forumName,
-			@Nullable String content) throws FormatException {
-		BdfDictionary meta = messageContext.getDictionary();
-		if (content == null) {
-			assertEquals(6, meta.size());
-		} else {
-			assertEquals(7, meta.size());
-			assertEquals(content, meta.getString(INVITATION_MSG));
-		}
-		assertEquals(forumName, meta.getString(FORUM_NAME));
-		assertEquals(salt, meta.getRaw(FORUM_SALT));
-		assertEquals(SHARE_MSG_TYPE_INVITATION, meta.getLong(TYPE).intValue());
-		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
-		assertFalse(meta.getBoolean(LOCAL));
-		assertEquals(timestamp, meta.getLong(TIME).longValue());
-		assertEquals(0, messageContext.getDependencies().size());
+	private void expectCreateForum(final String name) {
+		context.checking(new Expectations() {{
+			oneOf(forumFactory).createForum(name, salt);
+			will(returnValue(forum));
+		}});
+	}
+
+	private void expectEncodeMetadata(final MessageType type) {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder)
+					.encodeMetadata(type, groupId, timestamp, false, false,
+							false, false);
+			will(returnValue(meta));
+		}});
 	}
 
 	private void assertExpectedContext(BdfMessageContext messageContext,
-			int type) throws FormatException {
-		BdfDictionary meta = messageContext.getDictionary();
-		assertEquals(4, meta.size());
-		assertEquals(type, meta.getLong(TYPE).intValue());
-		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
-		assertFalse(meta.getBoolean(LOCAL));
-		assertEquals(timestamp, meta.getLong(TIME).longValue());
-		assertEquals(0, messageContext.getDependencies().size());
+			@Nullable MessageId previousMsgId) throws FormatException {
+		Collection<MessageId> dependencies = messageContext.getDependencies();
+		if (previousMsgId == null) {
+			assertTrue(dependencies.isEmpty());
+		} else {
+			assertEquals(1, dependencies.size());
+			assertTrue(dependencies.contains(previousMsgId));
+		}
+		assertEquals(meta, messageContext.getDictionary());
 	}
+
 }